[] _queues;
- private AsyncListener[] _listeners;
- private Scheduler _scheduler;
- private ServletContext _context;
-
- @Override
- public void init(FilterConfig filterConfig) throws ServletException
- {
- _queues = new Queue[getMaxPriority() + 1];
- _listeners = new AsyncListener[_queues.length];
- for (int p = 0; p < _queues.length; p++)
- {
- _queues[p] = new ConcurrentLinkedQueue<>();
- _listeners[p] = new DoSAsyncListener(p);
- }
-
- _rateTrackers.clear();
-
- int maxRequests = __DEFAULT_MAX_REQUESTS_PER_SEC;
- String parameter = filterConfig.getInitParameter(MAX_REQUESTS_PER_S_INIT_PARAM);
- if (parameter != null)
- maxRequests = Integer.parseInt(parameter);
- setMaxRequestsPerSec(maxRequests);
-
- long delay = __DEFAULT_DELAY_MS;
- parameter = filterConfig.getInitParameter(DELAY_MS_INIT_PARAM);
- if (parameter != null)
- delay = Long.parseLong(parameter);
- setDelayMs(delay);
-
- int throttledRequests = __DEFAULT_THROTTLE;
- parameter = filterConfig.getInitParameter(THROTTLED_REQUESTS_INIT_PARAM);
- if (parameter != null)
- throttledRequests = Integer.parseInt(parameter);
- setThrottledRequests(throttledRequests);
-
- long maxWait = __DEFAULT_MAX_WAIT_MS;
- parameter = filterConfig.getInitParameter(MAX_WAIT_INIT_PARAM);
- if (parameter != null)
- maxWait = Long.parseLong(parameter);
- setMaxWaitMs(maxWait);
-
- long throttle = __DEFAULT_THROTTLE_MS;
- parameter = filterConfig.getInitParameter(THROTTLE_MS_INIT_PARAM);
- if (parameter != null)
- throttle = Long.parseLong(parameter);
- setThrottleMs(throttle);
-
- long maxRequestMs = __DEFAULT_MAX_REQUEST_MS_INIT_PARAM;
- parameter = filterConfig.getInitParameter(MAX_REQUEST_MS_INIT_PARAM);
- if (parameter != null)
- maxRequestMs = Long.parseLong(parameter);
- setMaxRequestMs(maxRequestMs);
-
- long maxIdleTrackerMs = __DEFAULT_MAX_IDLE_TRACKER_MS_INIT_PARAM;
- parameter = filterConfig.getInitParameter(MAX_IDLE_TRACKER_MS_INIT_PARAM);
- if (parameter != null)
- maxIdleTrackerMs = Long.parseLong(parameter);
- setMaxIdleTrackerMs(maxIdleTrackerMs);
-
- String whiteList = "";
- parameter = filterConfig.getInitParameter(IP_WHITELIST_INIT_PARAM);
- if (parameter != null)
- whiteList = parameter;
- setWhitelist(whiteList);
-
- parameter = filterConfig.getInitParameter(INSERT_HEADERS_INIT_PARAM);
- setInsertHeaders(parameter == null || Boolean.parseBoolean(parameter));
-
- parameter = filterConfig.getInitParameter(TRACK_SESSIONS_INIT_PARAM);
- setTrackSessions(parameter == null || Boolean.parseBoolean(parameter));
-
- parameter = filterConfig.getInitParameter(REMOTE_PORT_INIT_PARAM);
- setRemotePort(parameter != null && Boolean.parseBoolean(parameter));
-
- parameter = filterConfig.getInitParameter(ENABLED_INIT_PARAM);
- setEnabled(parameter == null || Boolean.parseBoolean(parameter));
-
- parameter = filterConfig.getInitParameter(TOO_MANY_CODE);
- setTooManyCode(parameter == null ? 429 : Integer.parseInt(parameter));
-
- setName(filterConfig.getFilterName());
- _context = filterConfig.getServletContext();
- if (_context != null)
- {
- _context.setAttribute(filterConfig.getFilterName(), this);
- }
-
- _scheduler = startScheduler();
- }
-
- protected Scheduler startScheduler() throws ServletException
- {
- try
- {
- Scheduler result = new ScheduledExecutorScheduler(String.format("DoS-Scheduler-%x", hashCode()), false);
- result.start();
- return result;
- }
- catch (Exception x)
- {
- throw new ServletException(x);
- }
- }
-
- @Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException
- {
- doFilter((HttpServletRequest)request, (HttpServletResponse)response, filterChain);
- }
-
- protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException
- {
- if (!isEnabled())
- {
- filterChain.doFilter(request, response);
- return;
- }
-
- // Look for the rate tracker for this request.
- RateTracker tracker = (RateTracker)request.getAttribute(__TRACKER);
- if (tracker == null)
- {
- // This is the first time we have seen this request.
- if (LOG.isDebugEnabled())
- LOG.debug("Filtering {}", request);
-
- // Get a rate tracker associated with this request, and record one hit.
- tracker = getRateTracker(request);
-
- // Calculate the rate and check if it is over the allowed limit
- final boolean overRateLimit = tracker.isRateExceeded(System.currentTimeMillis());
-
- // Pass it through if we are not currently over the rate limit.
- if (!overRateLimit)
- {
- if (LOG.isDebugEnabled())
- LOG.debug("Allowing {}", request);
- doFilterChain(filterChain, request, response);
- return;
- }
-
- // We are over the limit.
-
- // So either reject it, delay it or throttle it.
- long delayMs = getDelayMs();
- boolean insertHeaders = isInsertHeaders();
- switch ((int)delayMs)
- {
- case -1:
- {
- // Reject this request.
- LOG.warn("DOS ALERT: Request rejected ip={}, session={}, user={}", request.getRemoteAddr(), request.getRequestedSessionId(), request.getUserPrincipal());
- if (insertHeaders)
- response.addHeader("DoSFilter", "unavailable");
- response.sendError(getTooManyCode());
- return;
- }
- case 0:
- {
- // Fall through to throttle the request.
- LOG.warn("DOS ALERT: Request throttled ip={}, session={}, user={}", request.getRemoteAddr(), request.getRequestedSessionId(), request.getUserPrincipal());
- request.setAttribute(__TRACKER, tracker);
- break;
- }
- default:
- {
- // Insert a delay before throttling the request,
- // using the suspend+timeout mechanism of AsyncContext.
- LOG.warn("DOS ALERT: Request delayed={}ms, ip={}, session={}, user={}", delayMs, request.getRemoteAddr(), request.getRequestedSessionId(), request.getUserPrincipal());
- if (insertHeaders)
- response.addHeader("DoSFilter", "delayed");
- request.setAttribute(__TRACKER, tracker);
- AsyncContext asyncContext = request.startAsync();
- if (delayMs > 0)
- asyncContext.setTimeout(delayMs);
- asyncContext.addListener(new DoSTimeoutAsyncListener());
- return;
- }
- }
- }
-
- if (LOG.isDebugEnabled())
- LOG.debug("Throttling {}", request);
-
- // Throttle the request.
- boolean accepted = false;
- try
- {
- // Check if we can afford to accept another request at this time.
- accepted = _passes.tryAcquire(getMaxWaitMs(), TimeUnit.MILLISECONDS);
- if (!accepted)
- {
- // We were not accepted, so either we suspend to wait,
- // or if we were woken up we insist or we fail.
- Boolean throttled = (Boolean)request.getAttribute(__THROTTLED);
- long throttleMs = getThrottleMs();
- if (!Boolean.TRUE.equals(throttled) && throttleMs > 0)
- {
- int priority = getPriority(request, tracker);
- request.setAttribute(__THROTTLED, Boolean.TRUE);
- if (isInsertHeaders())
- response.addHeader("DoSFilter", "throttled");
- AsyncContext asyncContext = request.startAsync();
- request.setAttribute(_suspended, Boolean.TRUE);
- asyncContext.setTimeout(throttleMs);
- asyncContext.addListener(_listeners[priority]);
- _queues[priority].add(asyncContext);
- if (LOG.isDebugEnabled())
- LOG.debug("Throttled {}, {}ms", request, throttleMs);
- return;
- }
-
- Boolean resumed = (Boolean)request.getAttribute(_resumed);
- if (Boolean.TRUE.equals(resumed))
- {
- // We were resumed, we wait for the next pass.
- _passes.acquire();
- accepted = true;
- }
- }
-
- // If we were accepted (either immediately or after throttle)...
- if (accepted)
- {
- // ...call the chain.
- if (LOG.isDebugEnabled())
- LOG.debug("Allowing {}", request);
- doFilterChain(filterChain, request, response);
- }
- else
- {
- // ...otherwise fail the request.
- if (LOG.isDebugEnabled())
- LOG.debug("Rejecting {}", request);
- if (isInsertHeaders())
- response.addHeader("DoSFilter", "unavailable");
- response.sendError(getTooManyCode());
- }
- }
- catch (InterruptedException e)
- {
- LOG.trace("IGNORED", e);
- response.sendError(getTooManyCode());
- }
- finally
- {
- if (accepted)
- {
- try
- {
- // Wake up the next highest priority request.
- for (int p = _queues.length - 1; p >= 0; --p)
- {
- AsyncContext asyncContext = _queues[p].poll();
- if (asyncContext != null)
- {
- ServletRequest candidate = asyncContext.getRequest();
- Boolean suspended = (Boolean)candidate.getAttribute(_suspended);
- if (Boolean.TRUE.equals(suspended))
- {
- if (LOG.isDebugEnabled())
- LOG.debug("Resuming {}", request);
- candidate.setAttribute(_resumed, Boolean.TRUE);
- asyncContext.dispatch();
- break;
- }
- }
- }
- }
- finally
- {
- _passes.release();
- }
- }
- }
- }
-
- protected void doFilterChain(FilterChain chain, final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException
- {
- final Thread thread = Thread.currentThread();
- Runnable requestTimeout = () -> onRequestTimeout(request, response, thread);
- Scheduler.Task task = _scheduler.schedule(requestTimeout, getMaxRequestMs(), TimeUnit.MILLISECONDS);
- try
- {
- chain.doFilter(request, response);
- }
- finally
- {
- task.cancel();
- }
- }
-
- /**
- * Invoked when the request handling exceeds {@link #getMaxRequestMs()}.
- *
- * By default, an HTTP 503 response is returned and the handling thread is interrupted.
- *
- * @param request the current request
- * @param response the current response
- * @param handlingThread the handling thread
- */
- protected void onRequestTimeout(HttpServletRequest request, HttpServletResponse response, Thread handlingThread)
- {
- try
- {
- if (LOG.isDebugEnabled())
- LOG.debug("Timing out {}", request);
- try
- {
- response.sendError(HttpStatus.SERVICE_UNAVAILABLE_503);
- }
- catch (IllegalStateException ise)
- {
- LOG.trace("IGNORED", ise);
- // abort instead
- response.sendError(-1);
- }
- }
- catch (Throwable x)
- {
- LOG.info("Failed to sendError", x);
- }
-
- handlingThread.interrupt();
- }
-
- /**
- * Get priority for this request, based on user type
- *
- * @param request the current request
- * @param tracker the rate tracker for this request
- * @return the priority for this request
- */
- private int getPriority(HttpServletRequest request, RateTracker tracker)
- {
- if (extractUserId(request) != null)
- return USER_AUTH;
- if (tracker != null)
- return tracker.getType();
- return USER_UNKNOWN;
- }
-
- /**
- * @return the maximum priority that we can assign to a request
- */
- protected int getMaxPriority()
- {
- return USER_AUTH;
- }
-
- private void schedule(RateTracker tracker)
- {
- _scheduler.schedule(tracker, getMaxIdleTrackerMs(), TimeUnit.MILLISECONDS);
- }
-
- /**
- * Return a request rate tracker associated with this connection; keeps
- * track of this connection's request rate. If this is not the first request
- * from this connection, return the existing object with the stored stats.
- * If it is the first request, then create a new request tracker.
- *
- * Assumes that each connection has an identifying characteristic, and goes
- * through them in order, taking the first that matches: user id (logged
- * in), session id, client IP address. Unidentifiable connections are lumped
- * into one.
- *
- * When a session expires, its rate tracker is automatically deleted.
- *
- * @param request the current request
- * @return the request rate tracker for the current connection
- */
- RateTracker getRateTracker(ServletRequest request)
- {
- HttpSession session = ((HttpServletRequest)request).getSession(false);
-
- String loadId = extractUserId(request);
- final int type;
- if (loadId != null)
- {
- type = USER_AUTH;
- }
- else
- {
- if (isTrackSessions() && session != null && !session.isNew())
- {
- loadId = session.getId();
- type = USER_SESSION;
- }
- else
- {
- loadId = isRemotePort() ? createRemotePortId(request) : request.getRemoteAddr();
- type = USER_IP;
- }
- }
-
- RateTracker tracker = _rateTrackers.get(loadId);
-
- if (tracker == null)
- {
- boolean allowed = checkWhitelist(request.getRemoteAddr());
- int maxRequestsPerSec = getMaxRequestsPerSec();
- tracker = allowed ? new FixedRateTracker(_context, _name, loadId, type, maxRequestsPerSec)
- : new RateTracker(_context, _name, loadId, type, maxRequestsPerSec);
- tracker.setContext(_context);
- RateTracker existing = _rateTrackers.putIfAbsent(loadId, tracker);
- if (existing != null)
- tracker = existing;
-
- if (type == USER_IP)
- {
- // USER_IP expiration from _rateTrackers is handled by the _scheduler
- _scheduler.schedule(tracker, getMaxIdleTrackerMs(), TimeUnit.MILLISECONDS);
- }
- else if (session != null)
- {
- // USER_SESSION expiration from _rateTrackers are handled by the HttpSessionBindingListener
- session.setAttribute(__TRACKER, tracker);
- }
- }
-
- return tracker;
- }
-
- private void addToRateTracker(RateTracker tracker)
- {
- _rateTrackers.put(tracker.getId(), tracker);
- }
-
- public void removeFromRateTracker(String id)
- {
- _rateTrackers.remove(id);
- }
-
- protected boolean checkWhitelist(String candidate)
- {
- for (String address : _whitelist)
- {
- if (address.contains("/"))
- {
- if (subnetMatch(address, candidate))
- return true;
- }
- else
- {
- if (address.equals(candidate))
- return true;
- }
- }
- return false;
- }
-
- protected boolean subnetMatch(String subnetAddress, String address)
- {
- Matcher cidrMatcher = CIDR_PATTERN.matcher(subnetAddress);
- if (!cidrMatcher.matches())
- return false;
-
- String subnet = cidrMatcher.group(1);
- int prefix;
- try
- {
- prefix = Integer.parseInt(cidrMatcher.group(2));
- }
- catch (NumberFormatException x)
- {
- LOG.info("Ignoring malformed CIDR address {}", subnetAddress);
- return false;
- }
-
- byte[] subnetBytes = addressToBytes(subnet);
- if (subnetBytes == null)
- {
- LOG.info("Ignoring malformed CIDR address {}", subnetAddress);
- return false;
- }
- byte[] addressBytes = addressToBytes(address);
- if (addressBytes == null)
- {
- LOG.info("Ignoring malformed remote address {}", address);
- return false;
- }
-
- // Comparing IPv4 with IPv6 ?
- int length = subnetBytes.length;
- if (length != addressBytes.length)
- return false;
-
- byte[] mask = prefixToBytes(prefix, length);
-
- for (int i = 0; i < length; ++i)
- {
- if ((subnetBytes[i] & mask[i]) != (addressBytes[i] & mask[i]))
- return false;
- }
-
- return true;
- }
-
- private byte[] addressToBytes(String address)
- {
- Matcher ipv4Matcher = IPv4_PATTERN.matcher(address);
- if (ipv4Matcher.matches())
- {
- byte[] result = new byte[4];
- for (int i = 0; i < result.length; ++i)
- {
- result[i] = Integer.valueOf(ipv4Matcher.group(i + 1)).byteValue();
- }
- return result;
- }
- else
- {
- Matcher ipv6Matcher = IPv6_PATTERN.matcher(address);
- if (ipv6Matcher.matches())
- {
- byte[] result = new byte[16];
- for (int i = 0; i < result.length; i += 2)
- {
- int word = Integer.parseInt(ipv6Matcher.group(i / 2 + 1), 16);
- result[i] = (byte)((word & 0xFF00) >>> 8);
- result[i + 1] = (byte)(word & 0xFF);
- }
- return result;
- }
- }
- return null;
- }
-
- private byte[] prefixToBytes(int prefix, int length)
- {
- byte[] result = new byte[length];
- int index = 0;
- while (prefix / 8 > 0)
- {
- result[index] = -1;
- prefix -= 8;
- ++index;
- }
-
- if (index == result.length)
- return result;
-
- // Sets the _prefix_ most significant bits to 1
- result[index] = (byte)~((1 << (8 - prefix)) - 1);
- return result;
- }
-
- @Override
- public void destroy()
- {
- LOG.debug("Destroy {}", this);
- stopScheduler();
- _rateTrackers.clear();
- _whitelist.clear();
- }
-
- protected void stopScheduler()
- {
- try
- {
- _scheduler.stop();
- }
- catch (Exception x)
- {
- LOG.trace("IGNORED", x);
- }
- }
-
- /**
- * Returns the user id, used to track this connection.
- * This SHOULD be overridden by subclasses.
- *
- * @param request the current request
- * @return a unique user id, if logged in; otherwise null.
- */
- protected String extractUserId(ServletRequest request)
- {
- return null;
- }
-
- /**
- * Get maximum number of requests from a connection per
- * second. Requests in excess of this are first delayed,
- * then throttled.
- *
- * @return maximum number of requests
- */
- @ManagedAttribute("maximum number of requests allowed from a connection per second")
- public int getMaxRequestsPerSec()
- {
- return _maxRequestsPerSec;
- }
-
- /**
- * Get maximum number of requests from a connection per
- * second. Requests in excess of this are first delayed,
- * then throttled.
- *
- * @param value maximum number of requests
- */
- public void setMaxRequestsPerSec(int value)
- {
- _maxRequestsPerSec = value;
- }
-
- /**
- * Get delay (in milliseconds) that is applied to all requests
- * over the rate limit, before they are considered at all.
- *
- * @return the delay in milliseconds
- */
- @ManagedAttribute("delay applied to all requests over the rate limit (in ms)")
- public long getDelayMs()
- {
- return _delayMs;
- }
-
- /**
- * Set delay (in milliseconds) that is applied to all requests
- * over the rate limit, before they are considered at all.
- *
- * @param value delay (in milliseconds), 0 - no delay, -1 - reject request
- */
- public void setDelayMs(long value)
- {
- _delayMs = value;
- }
-
- /**
- * Get maximum amount of time (in milliseconds) the filter will
- * blocking wait for the throttle semaphore.
- *
- * @return maximum wait time
- */
- @ManagedAttribute("maximum time the filter will block waiting throttled connections, (0 for no delay, -1 to reject requests)")
- public long getMaxWaitMs()
- {
- return _maxWaitMs;
- }
-
- /**
- * Set maximum amount of time (in milliseconds) the filter will
- * blocking wait for the throttle semaphore.
- *
- * @param value maximum wait time
- */
- public void setMaxWaitMs(long value)
- {
- _maxWaitMs = value;
- }
-
- /**
- * Get number of requests over the rate limit able to be
- * considered at once.
- *
- * @return number of requests
- */
- @ManagedAttribute("number of requests over rate limit")
- public int getThrottledRequests()
- {
- return _throttledRequests;
- }
-
- /**
- * Set number of requests over the rate limit able to be
- * considered at once.
- *
- * @param value number of requests
- */
- public void setThrottledRequests(int value)
- {
- int permits = _passes == null ? 0 : _passes.availablePermits();
- _passes = new Semaphore((value - _throttledRequests + permits), true);
- _throttledRequests = value;
- }
-
- /**
- * Get amount of time (in milliseconds) to async wait for semaphore.
- *
- * @return wait time
- */
- @ManagedAttribute("amount of time to async wait for semaphore")
- public long getThrottleMs()
- {
- return _throttleMs;
- }
-
- /**
- * Set amount of time (in milliseconds) to async wait for semaphore.
- *
- * @param value wait time
- */
- public void setThrottleMs(long value)
- {
- _throttleMs = value;
- }
-
- /**
- * Get maximum amount of time (in milliseconds) to allow
- * the request to process.
- *
- * @return maximum processing time
- */
- @ManagedAttribute("maximum time to allow requests to process (in ms)")
- public long getMaxRequestMs()
- {
- return _maxRequestMs;
- }
-
- /**
- * Set maximum amount of time (in milliseconds) to allow
- * the request to process.
- *
- * @param value maximum processing time
- */
- public void setMaxRequestMs(long value)
- {
- _maxRequestMs = value;
- }
-
- /**
- * Get maximum amount of time (in milliseconds) to keep track
- * of request rates for a connection, before deciding that
- * the user has gone away, and discarding it.
- *
- * @return maximum tracking time
- */
- @ManagedAttribute("maximum time to track of request rates for connection before discarding")
- public long getMaxIdleTrackerMs()
- {
- return _maxIdleTrackerMs;
- }
-
- /**
- * Set maximum amount of time (in milliseconds) to keep track
- * of request rates for a connection, before deciding that
- * the user has gone away, and discarding it.
- *
- * @param value maximum tracking time
- */
- public void setMaxIdleTrackerMs(long value)
- {
- _maxIdleTrackerMs = value;
- }
-
- /**
- * The unique name of the filter when there is more than
- * one DosFilter instance.
- *
- * @return the name
- */
- public String getName()
- {
- return _name;
- }
-
- /**
- * @param name the name to set
- */
- public void setName(String name)
- {
- _name = name;
- }
-
- /**
- * Check flag to insert the DoSFilter headers into the response.
- *
- * @return value of the flag
- */
- @ManagedAttribute("inser DoSFilter headers in response")
- public boolean isInsertHeaders()
- {
- return _insertHeaders;
- }
-
- /**
- * Set flag to insert the DoSFilter headers into the response.
- *
- * @param value value of the flag
- */
- public void setInsertHeaders(boolean value)
- {
- _insertHeaders = value;
- }
-
- /**
- * Get flag to have usage rate tracked by session if a session exists.
- *
- * @return value of the flag
- */
- @ManagedAttribute("usage rate is tracked by session if one exists")
- public boolean isTrackSessions()
- {
- return _trackSessions;
- }
-
- /**
- * Set flag to have usage rate tracked by session if a session exists.
- *
- * @param value value of the flag
- */
- public void setTrackSessions(boolean value)
- {
- _trackSessions = value;
- }
-
- /**
- * Get flag to have usage rate tracked by IP+port (effectively connection)
- * if session tracking is not used.
- *
- * @return value of the flag
- */
- @ManagedAttribute("usage rate is tracked by IP+port is session tracking not used")
- public boolean isRemotePort()
- {
- return _remotePort;
- }
-
- /**
- * Set flag to have usage rate tracked by IP+port (effectively connection)
- * if session tracking is not used.
- *
- * @param value value of the flag
- */
- public void setRemotePort(boolean value)
- {
- _remotePort = value;
- }
-
- /**
- * @return whether this filter is enabled
- */
- @ManagedAttribute("whether this filter is enabled")
- public boolean isEnabled()
- {
- return _enabled;
- }
-
- /**
- * @param enabled whether this filter is enabled
- */
- public void setEnabled(boolean enabled)
- {
- _enabled = enabled;
- }
-
- public int getTooManyCode()
- {
- return _tooManyCode;
- }
-
- public void setTooManyCode(int tooManyCode)
- {
- _tooManyCode = tooManyCode;
- }
-
- /**
- * Get a list of IP addresses that will not be rate limited.
- *
- * @return comma-separated whitelist
- */
- @ManagedAttribute("list of IPs that will not be rate limited")
- public String getWhitelist()
- {
- StringBuilder result = new StringBuilder();
- for (Iterator iterator = _whitelist.iterator(); iterator.hasNext(); )
- {
- String address = iterator.next();
- result.append(address);
- if (iterator.hasNext())
- result.append(",");
- }
- return result.toString();
- }
-
- /**
- * Set a list of IP addresses that will not be rate limited.
- *
- * @param commaSeparatedList comma-separated whitelist
- */
- public void setWhitelist(String commaSeparatedList)
- {
- List result = new ArrayList<>();
- for (String address : StringUtil.csvSplit(commaSeparatedList))
- {
- addWhitelistAddress(result, address);
- }
- clearWhitelist();
- _whitelist.addAll(result);
- LOG.debug("Whitelisted IP addresses: {}", result);
- }
-
- /**
- * Clears the list of whitelisted IP addresses
- */
- @ManagedOperation("clears the list of IP addresses that will not be rate limited")
- public void clearWhitelist()
- {
- _whitelist.clear();
- }
-
- /**
- * Adds the given IP address, either in the form of a dotted decimal notation A.B.C.D
- * or in the CIDR notation A.B.C.D/M, to the list of whitelisted IP addresses.
- *
- * @param address the address to add
- * @return whether the address was added to the list
- * @see #removeWhitelistAddress(String)
- */
- @ManagedOperation("adds an IP address that will not be rate limited")
- public boolean addWhitelistAddress(@Name("address") String address)
- {
- return addWhitelistAddress(_whitelist, address);
- }
-
- private boolean addWhitelistAddress(List list, String address)
- {
- address = address.trim();
- return address.length() > 0 && list.add(address);
- }
-
- /**
- * Removes the given address from the list of whitelisted IP addresses.
- *
- * @param address the address to remove
- * @return whether the address was removed from the list
- * @see #addWhitelistAddress(String)
- */
- @ManagedOperation("removes an IP address that will not be rate limited")
- public boolean removeWhitelistAddress(@Name("address") String address)
- {
- return _whitelist.remove(address);
- }
-
- /**
- * A RateTracker is associated with a connection, and stores request rate
- * data.
- */
- static class RateTracker implements Runnable, HttpSessionBindingListener, HttpSessionActivationListener, Serializable
- {
- private static final long serialVersionUID = 3534663738034577872L;
-
- protected final String _filterName;
- protected transient ServletContext _context;
- protected final String _id;
- protected final int _type;
- protected final long[] _timestamps;
-
- protected int _next;
-
- public RateTracker(ServletContext context, String filterName, String id, int type, int maxRequestsPerSecond)
- {
- _context = context;
- _filterName = filterName;
- _id = id;
- _type = type;
- _timestamps = new long[maxRequestsPerSecond];
- _next = 0;
- }
-
- /**
- * @param now the time now (in milliseconds)
- * @return the current calculated request rate over the last second
- */
- public boolean isRateExceeded(long now)
- {
- final long last;
- synchronized (this)
- {
- last = _timestamps[_next];
- _timestamps[_next] = now;
- _next = (_next + 1) % _timestamps.length;
- }
-
- return last != 0 && (now - last) < 1000L;
- }
-
- public String getId()
- {
- return _id;
- }
-
- public int getType()
- {
- return _type;
- }
-
- @Override
- public void valueBound(HttpSessionBindingEvent event)
- {
- if (LOG.isDebugEnabled())
- LOG.debug("Value bound: {}", getId());
- _context = event.getSession().getServletContext();
- }
-
- @Override
- public void valueUnbound(HttpSessionBindingEvent event)
- {
- //take the tracker out of the list of trackers
- DoSFilter filter = (DoSFilter)event.getSession().getServletContext().getAttribute(_filterName);
- removeFromRateTrackers(filter, _id);
- _context = null;
- }
-
- @Override
- public void sessionWillPassivate(HttpSessionEvent se)
- {
- //take the tracker of the list of trackers (if its still there)
- DoSFilter filter = (DoSFilter)se.getSession().getServletContext().getAttribute(_filterName);
- removeFromRateTrackers(filter, _id);
- _context = null;
- }
-
- @Override
- public void sessionDidActivate(HttpSessionEvent se)
- {
- RateTracker tracker = (RateTracker)se.getSession().getAttribute(__TRACKER);
- ServletContext context = se.getSession().getServletContext();
- tracker.setContext(context);
- DoSFilter filter = (DoSFilter)context.getAttribute(_filterName);
- if (filter == null)
- {
- LOG.info("No filter {} for rate tracker {}", _filterName, tracker);
- return;
- }
- addToRateTrackers(filter, tracker);
- }
-
- public void setContext(ServletContext context)
- {
- _context = context;
- }
-
- protected void removeFromRateTrackers(DoSFilter filter, String id)
- {
- if (filter == null)
- return;
-
- filter.removeFromRateTracker(id);
- if (LOG.isDebugEnabled())
- LOG.debug("Tracker removed: {}", getId());
- }
-
- private void addToRateTrackers(DoSFilter filter, RateTracker tracker)
- {
- if (filter == null)
- return;
- filter.addToRateTracker(tracker);
- }
-
- @Override
- public void run()
- {
- if (_context == null)
- {
- LOG.warn("Unknkown context for rate tracker {}", this);
- return;
- }
-
- int latestIndex = _next == 0 ? (_timestamps.length - 1) : (_next - 1);
- long last = _timestamps[latestIndex];
- boolean hasRecentRequest = last != 0 && (System.currentTimeMillis() - last) < 1000L;
-
- DoSFilter filter = (DoSFilter)_context.getAttribute(_filterName);
-
- if (hasRecentRequest)
- {
- if (filter != null)
- filter.schedule(this);
- else
- LOG.warn("No filter {}", _filterName);
- }
- else
- removeFromRateTrackers(filter, _id);
- }
-
- @Override
- public String toString()
- {
- return "RateTracker/" + _id + "/" + _type;
- }
- }
-
- private static class FixedRateTracker extends RateTracker
- {
- public FixedRateTracker(ServletContext context, String filterName, String id, int type, int numRecentRequestsTracked)
- {
- super(context, filterName, id, type, numRecentRequestsTracked);
- }
-
- @Override
- public boolean isRateExceeded(long now)
- {
- // rate limit is never exceeded, but we keep track of the request timestamps
- // so that we know whether there was recent activity on this tracker
- // and whether it should be expired
- synchronized (this)
- {
- _timestamps[_next] = now;
- _next = (_next + 1) % _timestamps.length;
- }
-
- return false;
- }
-
- @Override
- public String toString()
- {
- return "Fixed" + super.toString();
- }
- }
-
- private static class DoSTimeoutAsyncListener implements AsyncListener
- {
- @Override
- public void onStartAsync(AsyncEvent event)
- {
- }
-
- @Override
- public void onComplete(AsyncEvent event)
- {
- }
-
- @Override
- public void onTimeout(AsyncEvent event) throws IOException
- {
- event.getAsyncContext().dispatch();
- }
-
- @Override
- public void onError(AsyncEvent event)
- {
- }
- }
-
- private class DoSAsyncListener extends DoSTimeoutAsyncListener
- {
- private final int priority;
-
- public DoSAsyncListener(int priority)
- {
- this.priority = priority;
- }
-
- @Override
- public void onTimeout(AsyncEvent event) throws IOException
- {
- _queues[priority].remove(event.getAsyncContext());
- super.onTimeout(event);
- }
- }
-
- private String createRemotePortId(final ServletRequest request)
- {
- final String addr = request.getRemoteAddr();
- final int port = request.getRemotePort();
- if (addr.contains(":"))
- return "[" + addr + "]:" + port;
- return addr + ":" + port;
- }
-}
diff --git a/test-proxy/src/main/java/servlets/EventSource.java b/test-proxy/src/main/java/servlets/EventSource.java
deleted file mode 100644
index 5bf2390..0000000
--- a/test-proxy/src/main/java/servlets/EventSource.java
+++ /dev/null
@@ -1,108 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
-//
-// This program and the accompanying materials are made available under
-// the terms of the Eclipse Public License 2.0 which is available at
-// https://www.eclipse.org/legal/epl-2.0
-//
-// This Source Code may also be made available under the following
-// Secondary Licenses when the conditions for such availability set
-// forth in the Eclipse Public License, v. 2.0 are satisfied:
-// the Apache License v2.0 which is available at
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
-// ========================================================================
-//
-
-package servlets;
-
-import java.io.IOException;
-
-/**
- * {@link EventSource} is the passive half of an event source connection, as defined by the
- * EventSource Specification.
- * {@link EventSource.Emitter} is the active half of the connection and allows to operate on the connection.
- * {@link EventSource} allows applications to be notified of events happening on the connection;
- * two events are being notified: the opening of the event source connection, where method
- * {@link EventSource#onOpen(Emitter)} is invoked, and the closing of the event source connection,
- * where method {@link EventSource#onClose()} is invoked.
- *
- * @see EventSourceServlet
- */
-public interface EventSource
-{
- /**
- * Callback method invoked when an event source connection is opened.
- *
- * @param emitter the {@link Emitter} instance that allows to operate on the connection
- * @throws IOException if the implementation of the method throws such exception
- */
- public void onOpen(Emitter emitter) throws IOException;
-
- /**
- * Callback method invoked when an event source connection is closed.
- */
- public void onClose();
-
- /**
- * {@link Emitter} is the active half of an event source connection, and allows applications
- * to operate on the connection by sending events, data or comments, or by closing the connection.
- * An {@link Emitter} instance will be created for each new event source connection.
- * {@link Emitter} instances are fully thread safe and can be used from multiple threads.
- */
- public interface Emitter
- {
- /**
- * Sends a named event with data to the client.
- * When invoked as: event("foo", "bar")
, the client will receive the lines:
- *
- * event: foo
- * data: bar
- *
- *
- * @param name the event name
- * @param data the data to be sent
- * @throws IOException if an I/O failure occurred
- * @see #data(String)
- */
- public void event(String name, String data) throws IOException;
-
- /**
- * Sends a default event with data to the client.
- * When invoked as: data("baz")
, the client will receive the line:
- *
- * data: baz
- *
- * When invoked as: data("foo\r\nbar\rbaz\nbax")
, the client will receive the lines:
- *
- * data: foo
- * data: bar
- * data: baz
- * data: bax
- *
- *
- * @param data the data to be sent
- * @throws IOException if an I/O failure occurred
- */
- public void data(String data) throws IOException;
-
- /**
- * Sends a comment to the client.
- * When invoked as: comment("foo")
, the client will receive the line:
- *
- * : foo
- *
- *
- * @param comment the comment to send
- * @throws IOException if an I/O failure occurred
- */
- public void comment(String comment) throws IOException;
-
- /**
- * Closes this event source connection.
- */
- public void close();
- }
-}
diff --git a/test-proxy/src/main/java/servlets/EventSourceServlet.java b/test-proxy/src/main/java/servlets/EventSourceServlet.java
deleted file mode 100644
index 9344fa7..0000000
--- a/test-proxy/src/main/java/servlets/EventSourceServlet.java
+++ /dev/null
@@ -1,236 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
-//
-// This program and the accompanying materials are made available under
-// the terms of the Eclipse Public License 2.0 which is available at
-// https://www.eclipse.org/legal/epl-2.0
-//
-// This Source Code may also be made available under the following
-// Secondary Licenses when the conditions for such availability set
-// forth in the Eclipse Public License, v. 2.0 are satisfied:
-// the Apache License v2.0 which is available at
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
-// ========================================================================
-//
-
-package servlets;
-
-import javax.servlet.AsyncContext;
-import javax.servlet.ServletException;
-import javax.servlet.ServletOutputStream;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.StringReader;
-import java.nio.charset.StandardCharsets;
-import java.util.Enumeration;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-
-/**
- * A servlet that implements the event source protocol,
- * also known as "server sent events".
- * This servlet must be subclassed to implement abstract method {@link #newEventSource(HttpServletRequest)}
- * to return an instance of {@link EventSource} that allows application to listen for event source events
- * and to emit event source events.
- * This servlet supports the following configuration parameters:
- *
- * heartBeatPeriod
, that specifies the heartbeat period, in seconds, used to check
- * whether the connection has been closed by the client; defaults to 10 seconds.
- *
- *
- * NOTE: there is currently no support for last-event-id
.
- */
-public abstract class EventSourceServlet extends HttpServlet
-{
- private static final byte[] CRLF = new byte[]{'\r', '\n'};
- private static final byte[] EVENT_FIELD = "event: ".getBytes(StandardCharsets.UTF_8);
- private static final byte[] DATA_FIELD = "data: ".getBytes(StandardCharsets.UTF_8);
- private static final byte[] COMMENT_FIELD = ": ".getBytes(StandardCharsets.UTF_8);
-
- private ScheduledExecutorService scheduler;
- private int heartBeatPeriod = 10;
-
- @Override
- public void init() throws ServletException
- {
- String heartBeatPeriodParam = getServletConfig().getInitParameter("heartBeatPeriod");
- if (heartBeatPeriodParam != null)
- heartBeatPeriod = Integer.parseInt(heartBeatPeriodParam);
- scheduler = Executors.newSingleThreadScheduledExecutor();
- }
-
- @Override
- public void destroy()
- {
- if (scheduler != null)
- scheduler.shutdown();
- }
-
- @Override
- protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
- {
- @SuppressWarnings("unchecked") Enumeration acceptValues = request.getHeaders("Accept");
- while (acceptValues.hasMoreElements())
- {
- String accept = acceptValues.nextElement();
- if (accept.equals("text/event-stream"))
- {
- EventSource eventSource = newEventSource(request);
- if (eventSource == null)
- {
- response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
- }
- else
- {
- respond(request, response);
- AsyncContext async = request.startAsync();
- // Infinite timeout because the continuation is never resumed,
- // but only completed on close
- async.setTimeout(0);
- EventSourceEmitter emitter = new EventSourceEmitter(eventSource, async);
- emitter.scheduleHeartBeat();
- open(eventSource, emitter);
- }
- return;
- }
- }
- super.doGet(request, response);
- }
-
- protected abstract EventSource newEventSource(HttpServletRequest request);
-
- protected void respond(HttpServletRequest request, HttpServletResponse response) throws IOException
- {
- response.setStatus(HttpServletResponse.SC_OK);
- response.setCharacterEncoding(StandardCharsets.UTF_8.name());
- response.setContentType("text/event-stream");
- // By adding this header, and not closing the connection,
- // we disable HTTP chunking, and we can use write()+flush()
- // to send data in the text/event-stream protocol
- response.addHeader("Connection", "close");
- response.flushBuffer();
- }
-
- protected void open(EventSource eventSource, EventSource.Emitter emitter) throws IOException
- {
- eventSource.onOpen(emitter);
- }
-
- protected class EventSourceEmitter implements EventSource.Emitter, Runnable
- {
- private final EventSource eventSource;
- private final AsyncContext async;
- private final ServletOutputStream output;
- private Future> heartBeat;
- private boolean closed;
-
- public EventSourceEmitter(EventSource eventSource, AsyncContext async) throws IOException
- {
- this.eventSource = eventSource;
- this.async = async;
- this.output = async.getResponse().getOutputStream();
- }
-
- @Override
- public void event(String name, String data) throws IOException
- {
- synchronized (this)
- {
- output.write(EVENT_FIELD);
- output.write(name.getBytes(StandardCharsets.UTF_8));
- output.write(CRLF);
- data(data);
- }
- }
-
- @Override
- public void data(String data) throws IOException
- {
- synchronized (this)
- {
- BufferedReader reader = new BufferedReader(new StringReader(data));
- String line;
- while ((line = reader.readLine()) != null)
- {
- output.write(DATA_FIELD);
- output.write(line.getBytes(StandardCharsets.UTF_8));
- output.write(CRLF);
- }
- output.write(CRLF);
- flush();
- }
- }
-
- @Override
- public void comment(String comment) throws IOException
- {
- synchronized (this)
- {
- output.write(COMMENT_FIELD);
- output.write(comment.getBytes(StandardCharsets.UTF_8));
- output.write(CRLF);
- output.write(CRLF);
- flush();
- }
- }
-
- @Override
- public void run()
- {
- // If the other peer closes the connection, the first
- // flush() should generate a TCP reset that is detected
- // on the second flush()
- try
- {
- synchronized (this)
- {
- output.write('\r');
- flush();
- output.write('\n');
- flush();
- }
- // We could write, reschedule heartbeat
- scheduleHeartBeat();
- }
- catch (IOException x)
- {
- // The other peer closed the connection
- close();
- eventSource.onClose();
- }
- }
-
- protected void flush() throws IOException
- {
- async.getResponse().flushBuffer();
- }
-
- @Override
- public void close()
- {
- synchronized (this)
- {
- closed = true;
- heartBeat.cancel(false);
- }
- async.complete();
- }
-
- private void scheduleHeartBeat()
- {
- synchronized (this)
- {
- if (!closed)
- heartBeat = scheduler.schedule(this, heartBeatPeriod, TimeUnit.SECONDS);
- }
- }
- }
-}
diff --git a/test-proxy/src/main/java/servlets/HeaderFilter.java b/test-proxy/src/main/java/servlets/HeaderFilter.java
deleted file mode 100644
index 461aac8..0000000
--- a/test-proxy/src/main/java/servlets/HeaderFilter.java
+++ /dev/null
@@ -1,194 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
-//
-// This program and the accompanying materials are made available under
-// the terms of the Eclipse Public License 2.0 which is available at
-// https://www.eclipse.org/legal/epl-2.0
-//
-// This Source Code may also be made available under the following
-// Secondary Licenses when the conditions for such availability set
-// forth in the Eclipse Public License, v. 2.0 are satisfied:
-// the Apache License v2.0 which is available at
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
-// ========================================================================
-//
-
-package servlets;
-
-import org.eclipse.jetty.util.StringUtil;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.servlet.*;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Header Filter
- *
- * This filter sets or adds a header to the response.
- *
- * The {@code headerConfig} init param is a CSV of actions to perform on headers, with the following syntax:
- * [action] [header name]: [header value]
- * [action] can be one of set
, add
, setDate
, or addDate
- * The date actions will add the header value in milliseconds to the current system time before setting a date header.
- *
- * Below is an example value for headerConfig
:
- *
- *
- * set X-Frame-Options: DENY,
- * "add Cache-Control: no-cache, no-store, must-revalidate",
- * setDate Expires: 31540000000,
- * addDate Date: 0
- *
- *
- * @see IncludeExcludeBasedFilter
- */
-public class HeaderFilter extends IncludeExcludeBasedFilter
-{
- private List _configuredHeaders = new ArrayList<>();
- private static final Logger LOG = LoggerFactory.getLogger(HeaderFilter.class);
-
- @Override
- public void init(FilterConfig filterConfig) throws ServletException
- {
- super.init(filterConfig);
- String headerConfig = filterConfig.getInitParameter("headerConfig");
-
- if (headerConfig != null)
- {
- String[] configs = StringUtil.csvSplit(headerConfig);
- for (String config : configs)
- {
- _configuredHeaders.add(parseHeaderConfiguration(config));
- }
- }
-
- if (LOG.isDebugEnabled())
- LOG.debug(this.toString());
- }
-
- @Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
- {
- HttpServletRequest httpRequest = (HttpServletRequest)request;
- HttpServletResponse httpResponse = (HttpServletResponse)response;
-
- if (super.shouldFilter(httpRequest, httpResponse))
- {
- for (ConfiguredHeader header : _configuredHeaders)
- {
- if (header.isDate())
- {
- long headerValue = System.currentTimeMillis() + header.getMsOffset();
- if (header.isAdd())
- {
- httpResponse.addDateHeader(header.getName(), headerValue);
- }
- else
- {
- httpResponse.setDateHeader(header.getName(), headerValue);
- }
- }
- else // constant header value
- {
- if (header.isAdd())
- {
- httpResponse.addHeader(header.getName(), header.getValue());
- }
- else
- {
- httpResponse.setHeader(header.getName(), header.getValue());
- }
- }
- }
- }
-
- chain.doFilter(request, response);
- }
-
- @Override
- public String toString()
- {
- StringBuilder sb = new StringBuilder();
- sb.append(super.toString()).append("\n");
- sb.append("configured headers:\n");
- for (ConfiguredHeader c : _configuredHeaders)
- {
- sb.append(c).append("\n");
- }
-
- return sb.toString();
- }
-
- private ConfiguredHeader parseHeaderConfiguration(String config)
- {
- String[] configTokens = config.trim().split(" ", 2);
- String method = configTokens[0].trim();
- String header = configTokens[1];
- String[] headerTokens = header.trim().split(":", 2);
- String headerName = headerTokens[0].trim();
- String headerValue = headerTokens[1].trim();
- ConfiguredHeader configuredHeader = new ConfiguredHeader(headerName, headerValue, method.startsWith("add"), method.endsWith("Date"));
- return configuredHeader;
- }
-
- private static class ConfiguredHeader
- {
- private String _name;
- private String _value;
- private long _msOffset;
- private boolean _add;
- private boolean _date;
-
- public ConfiguredHeader(String name, String value, boolean add, boolean date)
- {
- _name = name;
- _value = value;
- _add = add;
- _date = date;
-
- if (_date)
- {
- _msOffset = Long.parseLong(_value);
- }
- }
-
- public String getName()
- {
- return _name;
- }
-
- public String getValue()
- {
- return _value;
- }
-
- public boolean isAdd()
- {
- return _add;
- }
-
- public boolean isDate()
- {
- return _date;
- }
-
- public long getMsOffset()
- {
- return _msOffset;
- }
-
- @Override
- public String toString()
- {
- return (_add ? "add" : "set") + (_date ? "Date" : "") + " " + _name + ": " + _value;
- }
- }
-}
diff --git a/test-proxy/src/main/java/servlets/IncludeExcludeBasedFilter.java b/test-proxy/src/main/java/servlets/IncludeExcludeBasedFilter.java
deleted file mode 100644
index 77405f6..0000000
--- a/test-proxy/src/main/java/servlets/IncludeExcludeBasedFilter.java
+++ /dev/null
@@ -1,185 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
-//
-// This program and the accompanying materials are made available under
-// the terms of the Eclipse Public License 2.0 which is available at
-// https://www.eclipse.org/legal/epl-2.0
-//
-// This Source Code may also be made available under the following
-// Secondary Licenses when the conditions for such availability set
-// forth in the Eclipse Public License, v. 2.0 are satisfied:
-// the Apache License v2.0 which is available at
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
-// ========================================================================
-//
-
-package servlets;
-
-import org.eclipse.jetty.http.MimeTypes;
-import org.eclipse.jetty.http.pathmap.PathSpecSet;
-import org.eclipse.jetty.util.IncludeExclude;
-import org.eclipse.jetty.util.IncludeExcludeSet;
-import org.eclipse.jetty.util.StringUtil;
-import org.eclipse.jetty.util.URIUtil;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.servlet.Filter;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletContext;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-/**
- * Include Exclude Based Filter
- *
- * This is an abstract filter which helps with filtering based on include/exclude of paths, mime types, and/or http methods.
- *
- * Use the {@link #shouldFilter(HttpServletRequest, HttpServletResponse)} method to determine if a request/response should be filtered. If mime types are used,
- * it should be called after {@link javax.servlet.FilterChain#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} since the mime type may not
- * be written until then.
- *
- * Supported init params:
- *
- * includedPaths
- CSV of path specs to include
- * excludedPaths
- CSV of path specs to exclude
- * includedMimeTypes
- CSV of mime types to include
- * excludedMimeTypes
- CSV of mime types to exclude
- * includedHttpMethods
- CSV of http methods to include
- * excludedHttpMethods
- CSV of http methods to exclude
- *
- *
- * Path spec rules:
- *
- * - If the spec starts with
'^'
the spec is assumed to be a regex based path spec and will match with normal Java regex rules.
- * - If the spec starts with
'/'
the spec is assumed to be a Servlet url-pattern rules path spec for either an exact match or prefix based
- * match.
- * - If the spec starts with
'*.'
the spec is assumed to be a Servlet url-pattern rules path spec for a suffix based match.
- * - All other syntaxes are unsupported.
- *
- *
- * CSVs are parsed with {@link StringUtil#csvSplit(String)}
- *
- * @see PathSpecSet
- * @see IncludeExcludeSet
- */
-public abstract class IncludeExcludeBasedFilter implements Filter
-{
- private final IncludeExclude _mimeTypes = new IncludeExclude<>();
- private final IncludeExclude _httpMethods = new IncludeExclude<>();
- private final IncludeExclude _paths = new IncludeExclude<>(PathSpecSet.class);
- private static final Logger LOG = LoggerFactory.getLogger(IncludeExcludeBasedFilter.class);
-
- @Override
- public void init(FilterConfig filterConfig) throws ServletException
- {
- final String includedPaths = filterConfig.getInitParameter("includedPaths");
- final String excludedPaths = filterConfig.getInitParameter("excludedPaths");
- final String includedMimeTypes = filterConfig.getInitParameter("includedMimeTypes");
- final String excludedMimeTypes = filterConfig.getInitParameter("excludedMimeTypes");
- final String includedHttpMethods = filterConfig.getInitParameter("includedHttpMethods");
- final String excludedHttpMethods = filterConfig.getInitParameter("excludedHttpMethods");
-
- if (includedPaths != null)
- {
- _paths.include(StringUtil.csvSplit(includedPaths));
- }
- if (excludedPaths != null)
- {
- _paths.exclude(StringUtil.csvSplit(excludedPaths));
- }
- if (includedMimeTypes != null)
- {
- _mimeTypes.include(StringUtil.csvSplit(includedMimeTypes));
- }
- if (excludedMimeTypes != null)
- {
- _mimeTypes.exclude(StringUtil.csvSplit(excludedMimeTypes));
- }
- if (includedHttpMethods != null)
- {
- _httpMethods.include(StringUtil.csvSplit(includedHttpMethods));
- }
- if (excludedHttpMethods != null)
- {
- _httpMethods.exclude(StringUtil.csvSplit(excludedHttpMethods));
- }
- }
-
- protected String guessMimeType(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
- {
- String contentType = httpResponse.getContentType();
- LOG.debug("Content Type is: {}", contentType);
-
- String mimeType = "";
- if (contentType != null)
- {
- mimeType = MimeTypes.getContentTypeWithoutCharset(contentType);
- LOG.debug("Mime Type is: {}", mimeType);
- }
- else
- {
- String requestUrl = httpRequest.getPathInfo();
- mimeType = MimeTypes.getDefaultMimeByExtension(requestUrl);
-
- if (mimeType == null)
- {
- mimeType = "";
- }
-
- LOG.debug("Guessed mime type is {}", mimeType);
- }
-
- return mimeType;
- }
-
- protected boolean shouldFilter(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
- {
- String httpMethod = httpRequest.getMethod();
- LOG.debug("HTTP method is: {}", httpMethod);
- if (!_httpMethods.test(httpMethod))
- {
- LOG.debug("should not apply filter because HTTP method does not match");
- return false;
- }
-
- String mimeType = guessMimeType(httpRequest, httpResponse);
-
- if (!_mimeTypes.test(mimeType))
- {
- LOG.debug("should not apply filter because mime type does not match");
- return false;
- }
-
- ServletContext context = httpRequest.getServletContext();
- String path = context == null ? httpRequest.getRequestURI() : URIUtil.addPaths(httpRequest.getServletPath(), httpRequest.getPathInfo());
- LOG.debug("Path is: {}", path);
- if (!_paths.test(path))
- {
- LOG.debug("should not apply filter because path does not match");
- return false;
- }
-
- return true;
- }
-
- @Override
- public void destroy()
- {
- }
-
- @Override
- public String toString()
- {
- StringBuilder sb = new StringBuilder();
- sb.append("filter configuration:\n");
- sb.append("paths:\n").append(_paths).append("\n");
- sb.append("mime types:\n").append(_mimeTypes).append("\n");
- sb.append("http methods:\n").append(_httpMethods);
- return sb.toString();
- }
-}
diff --git a/test-proxy/src/main/java/servlets/PushCacheFilter.java b/test-proxy/src/main/java/servlets/PushCacheFilter.java
deleted file mode 100644
index d78a544..0000000
--- a/test-proxy/src/main/java/servlets/PushCacheFilter.java
+++ /dev/null
@@ -1,306 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
-//
-// This program and the accompanying materials are made available under
-// the terms of the Eclipse Public License 2.0 which is available at
-// https://www.eclipse.org/legal/epl-2.0
-//
-// This Source Code may also be made available under the following
-// Secondary Licenses when the conditions for such availability set
-// forth in the Eclipse Public License, v. 2.0 are satisfied:
-// the Apache License v2.0 which is available at
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
-// ========================================================================
-//
-
-package servlets;
-
-import org.eclipse.jetty.http.*;
-import org.eclipse.jetty.util.StringUtil;
-import org.eclipse.jetty.util.annotation.ManagedAttribute;
-import org.eclipse.jetty.util.annotation.ManagedObject;
-import org.eclipse.jetty.util.annotation.ManagedOperation;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.servlet.*;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.PushBuilder;
-import java.io.IOException;
-import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicLong;
-
-/**
- * A filter that builds a cache of secondary resources associated
- * to primary resources.
- * A typical request for a primary resource such as {@code index.html}
- * is immediately followed by a number of requests for secondary resources.
- * Secondary resource requests will have a {@code Referer} HTTP header
- * that points to {@code index.html}, which is used to associate the secondary
- * resource to the primary resource.
- * Only secondary resources that are requested within a (small) time period
- * from the request of the primary resource are associated with the primary
- * resource.
- * This allows to build a cache of secondary resources associated with
- * primary resources. When a request for a primary resource arrives, associated
- * secondary resources are pushed to the client, unless the request carries
- * {@code If-xxx} header that hint that the client has the resources in its
- * cache.
- * If the init param useQueryInKey is set, then the query string is used as
- * as part of the key to identify a resource
- */
-@ManagedObject("Push cache based on the HTTP 'Referer' header")
-public class PushCacheFilter implements Filter
-{
- private static final Logger LOG = LoggerFactory.getLogger(PushCacheFilter.class);
-
- private final Set _ports = new HashSet<>();
- private final Set _hosts = new HashSet<>();
- private final ConcurrentMap _cache = new ConcurrentHashMap<>();
- private long _associatePeriod = 4000L;
- private int _maxAssociations = 16;
- private long _renew = System.nanoTime();
- private boolean _useQueryInKey;
-
- @Override
- public void init(FilterConfig config) throws ServletException
- {
- String associatePeriod = config.getInitParameter("associatePeriod");
- if (associatePeriod != null)
- _associatePeriod = Long.parseLong(associatePeriod);
-
- String maxAssociations = config.getInitParameter("maxAssociations");
- if (maxAssociations != null)
- _maxAssociations = Integer.parseInt(maxAssociations);
-
- String hosts = config.getInitParameter("hosts");
- if (hosts != null)
- Collections.addAll(_hosts, StringUtil.csvSplit(hosts));
-
- String ports = config.getInitParameter("ports");
- if (ports != null)
- for (String p : StringUtil.csvSplit(ports))
- {
- _ports.add(Integer.parseInt(p));
- }
-
- _useQueryInKey = Boolean.parseBoolean(config.getInitParameter("useQueryInKey"));
-
- // Expose for JMX.
- config.getServletContext().setAttribute(config.getFilterName(), this);
-
- if (LOG.isDebugEnabled())
- LOG.debug("period={} max={} hosts={} ports={}", _associatePeriod, _maxAssociations, _hosts, _ports);
- }
-
- @Override
- public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException
- {
- HttpServletRequest request = (HttpServletRequest)req;
-
- PushBuilder pushBuilder = request.newPushBuilder();
- if (HttpVersion.fromString(request.getProtocol()).getVersion() < 20 ||
- !HttpMethod.GET.is(request.getMethod()) ||
- pushBuilder == null)
- {
- chain.doFilter(req, resp);
- return;
- }
-
- long now = System.nanoTime();
-
- boolean conditional = false;
- String referrer = null;
- List headerNames = Collections.list(request.getHeaderNames());
- for (String headerName : headerNames)
- {
- if (HttpHeader.IF_MATCH.is(headerName) ||
- HttpHeader.IF_MODIFIED_SINCE.is(headerName) ||
- HttpHeader.IF_NONE_MATCH.is(headerName) ||
- HttpHeader.IF_UNMODIFIED_SINCE.is(headerName))
- {
- conditional = true;
- break;
- }
- else if (HttpHeader.REFERER.is(headerName))
- {
- referrer = request.getHeader(headerName);
- }
- }
-
- if (LOG.isDebugEnabled())
- LOG.debug("{} {} referrer={} conditional={}", request.getMethod(), request.getRequestURI(), referrer, conditional);
-
- String path = request.getRequestURI();
- String query = request.getQueryString();
- if (_useQueryInKey && query != null)
- path += "?" + query;
- if (referrer != null)
- {
- HttpURI referrerURI = new HttpURI(referrer);
- String host = referrerURI.getHost();
- int port = referrerURI.getPort();
- if (port <= 0)
- {
- String scheme = referrerURI.getScheme();
- if (scheme != null)
- port = HttpScheme.HTTPS.is(scheme) ? 443 : 80;
- else
- port = request.isSecure() ? 443 : 80;
- }
-
- boolean referredFromHere = !_hosts.isEmpty() ? _hosts.contains(host) : host.equals(request.getServerName());
- referredFromHere &= !_ports.isEmpty() ? _ports.contains(port) : port == request.getServerPort();
-
- if (referredFromHere)
- {
- if (HttpMethod.GET.is(request.getMethod()))
- {
- String referrerPath = _useQueryInKey ? referrerURI.getPathQuery() : referrerURI.getPath();
- if (referrerPath == null)
- referrerPath = "/";
- if (referrerPath.startsWith(request.getContextPath() + "/"))
- {
- if (!referrerPath.equals(path))
- {
- PrimaryResource primaryResource = _cache.get(referrerPath);
- if (primaryResource != null)
- {
- long primaryTimestamp = primaryResource._timestamp.get();
- if (primaryTimestamp != 0)
- {
- if (now - primaryTimestamp < TimeUnit.MILLISECONDS.toNanos(_associatePeriod))
- {
- Set associated = primaryResource._associated;
- // Not strictly concurrent-safe, just best effort to limit associations.
- if (associated.size() <= _maxAssociations)
- {
- if (associated.add(path))
- {
- if (LOG.isDebugEnabled())
- LOG.debug("Associated {} to {}", path, referrerPath);
- }
- }
- else
- {
- if (LOG.isDebugEnabled())
- LOG.debug("Not associated {} to {}, exceeded max associations of {}", path, referrerPath, _maxAssociations);
- }
- }
- else
- {
- if (LOG.isDebugEnabled())
- LOG.debug("Not associated {} to {}, outside associate period of {}ms", path, referrerPath, _associatePeriod);
- }
- }
- }
- }
- else
- {
- if (LOG.isDebugEnabled())
- LOG.debug("Not associated {} to {}, referring to self", path, referrerPath);
- }
- }
- else
- {
- if (LOG.isDebugEnabled())
- LOG.debug("Not associated {} to {}, different context", path, referrerPath);
- }
- }
- }
- else
- {
- if (LOG.isDebugEnabled())
- LOG.debug("External referrer {}", referrer);
- }
- }
-
- PrimaryResource primaryResource = _cache.get(path);
- if (primaryResource == null)
- {
- PrimaryResource r = new PrimaryResource();
- primaryResource = _cache.putIfAbsent(path, r);
- primaryResource = primaryResource == null ? r : primaryResource;
- primaryResource._timestamp.compareAndSet(0, now);
- if (LOG.isDebugEnabled())
- LOG.debug("Cached primary resource {}", path);
- }
- else
- {
- long last = primaryResource._timestamp.get();
- if (last < _renew && primaryResource._timestamp.compareAndSet(last, now))
- {
- primaryResource._associated.clear();
- if (LOG.isDebugEnabled())
- LOG.debug("Clear associated resources for {}", path);
- }
- }
-
- // Push associated resources.
- if (!conditional && !primaryResource._associated.isEmpty())
- {
- // Breadth-first push of associated resources.
- Queue queue = new ArrayDeque<>();
- queue.offer(primaryResource);
- while (!queue.isEmpty())
- {
- PrimaryResource parent = queue.poll();
- for (String childPath : parent._associated)
- {
- PrimaryResource child = _cache.get(childPath);
- if (child != null)
- queue.offer(child);
-
- if (LOG.isDebugEnabled())
- LOG.debug("Pushing {} for {}", childPath, path);
- pushBuilder.path(childPath).push();
- }
- }
- }
-
- chain.doFilter(request, resp);
- }
-
- @Override
- public void destroy()
- {
- clearPushCache();
- }
-
- @ManagedAttribute("The push cache contents")
- public Map getPushCache()
- {
- Map result = new HashMap<>();
- for (Map.Entry entry : _cache.entrySet())
- {
- PrimaryResource resource = entry.getValue();
- String value = String.format("size=%d: %s", resource._associated.size(), new TreeSet<>(resource._associated));
- result.put(entry.getKey(), value);
- }
- return result;
- }
-
- @ManagedOperation(value = "Renews the push cache contents", impact = "ACTION")
- public void renewPushCache()
- {
- _renew = System.nanoTime();
- }
-
- @ManagedOperation(value = "Clears the push cache contents", impact = "ACTION")
- public void clearPushCache()
- {
- _cache.clear();
- }
-
- private static class PrimaryResource
- {
- private final Set _associated = Collections.newSetFromMap(new ConcurrentHashMap<>());
- private final AtomicLong _timestamp = new AtomicLong();
- }
-}
diff --git a/test-proxy/src/main/java/servlets/PushSessionCacheFilter.java b/test-proxy/src/main/java/servlets/PushSessionCacheFilter.java
deleted file mode 100644
index 9d71bb9..0000000
--- a/test-proxy/src/main/java/servlets/PushSessionCacheFilter.java
+++ /dev/null
@@ -1,194 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
-//
-// This program and the accompanying materials are made available under
-// the terms of the Eclipse Public License 2.0 which is available at
-// https://www.eclipse.org/legal/epl-2.0
-//
-// This Source Code may also be made available under the following
-// Secondary Licenses when the conditions for such availability set
-// forth in the Eclipse Public License, v. 2.0 are satisfied:
-// the Apache License v2.0 which is available at
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
-// ========================================================================
-//
-
-package servlets;
-
-import org.eclipse.jetty.http.HttpHeader;
-import org.eclipse.jetty.http.HttpURI;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.servlet.*;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
-import javax.servlet.http.PushBuilder;
-import java.io.IOException;
-import java.util.ArrayDeque;
-import java.util.Queue;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.TimeUnit;
-
-public class PushSessionCacheFilter implements Filter
-{
- private static final String RESPONSE_ATTR = "PushSessionCacheFilter.response";
- private static final String TARGET_ATTR = "PushSessionCacheFilter.target";
- private static final String TIMESTAMP_ATTR = "PushSessionCacheFilter.timestamp";
- private static final Logger LOG = LoggerFactory.getLogger(PushSessionCacheFilter.class);
- private final ConcurrentMap _cache = new ConcurrentHashMap<>();
- private long _associateDelay = 5000L;
-
- @Override
- public void init(FilterConfig config) throws ServletException
- {
- if (config.getInitParameter("associateDelay") != null)
- _associateDelay = Long.parseLong(config.getInitParameter("associateDelay"));
-
- // Add a listener that is used to collect information
- // about associated resource, etags and modified dates.
- config.getServletContext().addListener(new ServletRequestListener()
- {
- // Collect information when request is destroyed.
- @Override
- public void requestDestroyed(ServletRequestEvent sre)
- {
- HttpServletRequest request = (HttpServletRequest)sre.getServletRequest();
- Target target = (Target)request.getAttribute(TARGET_ATTR);
- if (target == null)
- return;
-
- // Update conditional data.
- HttpServletResponse response = (HttpServletResponse)request.getAttribute(RESPONSE_ATTR);
- target._etag = response.getHeader(HttpHeader.ETAG.asString());
- target._lastModified = response.getHeader(HttpHeader.LAST_MODIFIED.asString());
-
- if (LOG.isDebugEnabled())
- LOG.debug("Served {} for {}", response.getStatus(), request.getRequestURI());
-
- // Does this request have a referer?
- String referer = request.getHeader(HttpHeader.REFERER.asString());
- if (referer != null)
- {
- // Is the referer from this contexts?
- HttpURI refererUri = new HttpURI(referer);
- if (request.getServerName().equals(refererUri.getHost()))
- {
- Target refererTarget = _cache.get(refererUri.getPath());
- if (refererTarget != null)
- {
- HttpSession session = request.getSession();
- @SuppressWarnings("unchecked") ConcurrentHashMap timestamps = (ConcurrentHashMap)session.getAttribute(TIMESTAMP_ATTR);
- Long last = timestamps.get(refererTarget._path);
- if (last != null && TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - last) < _associateDelay)
- {
- if (refererTarget._associated.putIfAbsent(target._path, target) == null)
- {
- if (LOG.isDebugEnabled())
- LOG.debug("ASSOCIATE {}->{}", refererTarget._path, target._path);
- }
- }
- }
- }
- }
- }
-
- @Override
- public void requestInitialized(ServletRequestEvent sre)
- {
- }
- });
- }
-
- @Override
- public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException
- {
- req.setAttribute(RESPONSE_ATTR, resp);
- HttpServletRequest request = (HttpServletRequest)req;
- String uri = request.getRequestURI();
-
- if (LOG.isDebugEnabled())
- LOG.debug("{} {}", request.getMethod(), uri);
-
- HttpSession session = request.getSession(true);
-
- // find the target for this resource
- Target target = _cache.get(uri);
- if (target == null)
- {
- Target t = new Target(uri);
- target = _cache.putIfAbsent(uri, t);
- target = target == null ? t : target;
- }
- request.setAttribute(TARGET_ATTR, target);
-
- // Set the timestamp for this resource in this session
- @SuppressWarnings("unchecked") ConcurrentHashMap timestamps = (ConcurrentHashMap)session.getAttribute(TIMESTAMP_ATTR);
- if (timestamps == null)
- {
- timestamps = new ConcurrentHashMap<>();
- session.setAttribute(TIMESTAMP_ATTR, timestamps);
- }
- timestamps.put(uri, System.nanoTime());
-
- // Push any associated resources.
- PushBuilder builder = request.newPushBuilder();
- if (builder != null && !target._associated.isEmpty())
- {
- boolean conditional = request.getHeader(HttpHeader.IF_NONE_MATCH.asString()) != null ||
- request.getHeader(HttpHeader.IF_MODIFIED_SINCE.asString()) != null;
- // Breadth-first push of associated resources.
- Queue queue = new ArrayDeque<>();
- queue.offer(target);
- while (!queue.isEmpty())
- {
- Target parent = queue.poll();
- builder.addHeader("X-Pusher", PushSessionCacheFilter.class.toString());
- for (Target child : parent._associated.values())
- {
- queue.offer(child);
-
- String path = child._path;
- if (LOG.isDebugEnabled())
- LOG.debug("PUSH {} <- {}", path, uri);
-
- builder.path(path)
- .setHeader(HttpHeader.IF_NONE_MATCH.asString(), conditional ? child._etag : null)
- .setHeader(HttpHeader.IF_MODIFIED_SINCE.asString(), conditional ? child._lastModified : null);
- }
- }
- }
-
- chain.doFilter(req, resp);
- }
-
- @Override
- public void destroy()
- {
- _cache.clear();
- }
-
- private static class Target
- {
- private final String _path;
- private final ConcurrentMap _associated = new ConcurrentHashMap<>();
- private volatile String _etag;
- private volatile String _lastModified;
-
- private Target(String path)
- {
- _path = path;
- }
-
- @Override
- public String toString()
- {
- return String.format("Target{p=%s,e=%s,m=%s,a=%d}", _path, _etag, _lastModified, _associated.size());
- }
- }
-}
diff --git a/test-proxy/src/main/java/servlets/PutFilter.java b/test-proxy/src/main/java/servlets/PutFilter.java
deleted file mode 100644
index 85137f3..0000000
--- a/test-proxy/src/main/java/servlets/PutFilter.java
+++ /dev/null
@@ -1,356 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
-//
-// This program and the accompanying materials are made available under
-// the terms of the Eclipse Public License 2.0 which is available at
-// https://www.eclipse.org/legal/epl-2.0
-//
-// This Source Code may also be made available under the following
-// Secondary Licenses when the conditions for such availability set
-// forth in the Eclipse Public License, v. 2.0 are satisfied:
-// the Apache License v2.0 which is available at
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
-// ========================================================================
-//
-
-package servlets;
-
-import org.eclipse.jetty.util.IO;
-import org.eclipse.jetty.util.StringUtil;
-import org.eclipse.jetty.util.URIUtil;
-
-import javax.servlet.*;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpServletResponseWrapper;
-import java.io.*;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-
-/**
- * PutFilter
- *
- * A Filter that handles PUT, DELETE and MOVE methods.
- * Files are hidden during PUT operations, so that 404's result.
- *
- * The following init parameters pay be used:
- * - baseURI - The file URI of the document root for put content.
- *
- delAllowed - boolean, if true DELETE and MOVE methods are supported.
- *
- putAtomic - boolean, if true PUT files are written to a temp location and moved into place.
- *
- */
-public class PutFilter implements Filter
-{
- public static final String __PUT = "PUT";
- public static final String __DELETE = "DELETE";
- public static final String __MOVE = "MOVE";
- public static final String __OPTIONS = "OPTIONS";
-
- Set _operations = new HashSet();
- private ConcurrentMap _hidden = new ConcurrentHashMap();
-
- private ServletContext _context;
- private String _baseURI;
- private boolean _delAllowed;
- private boolean _putAtomic;
- private File _tmpdir;
-
- @Override
- public void init(FilterConfig config) throws ServletException
- {
- _context = config.getServletContext();
-
- _tmpdir = (File)_context.getAttribute("javax.servlet.context.tempdir");
-
- if (_context.getRealPath("/") == null)
- throw new UnavailableException("Packed war");
-
- String b = config.getInitParameter("baseURI");
- if (b != null)
- {
- _baseURI = b;
- }
- else
- {
- File base = new File(_context.getRealPath("/"));
- _baseURI = base.toURI().toString();
- }
-
- _delAllowed = getInitBoolean(config, "delAllowed");
- _putAtomic = getInitBoolean(config, "putAtomic");
-
- _operations.add(__OPTIONS);
- _operations.add(__PUT);
- if (_delAllowed)
- {
- _operations.add(__DELETE);
- _operations.add(__MOVE);
- }
- }
-
- private boolean getInitBoolean(FilterConfig config, String name)
- {
- String value = config.getInitParameter(name);
- return value != null && value.length() > 0 && (value.startsWith("t") || value.startsWith("T") || value.startsWith("y") || value.startsWith("Y") || value.startsWith("1"));
- }
-
- @Override
- public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException
- {
- HttpServletRequest request = (HttpServletRequest)req;
- HttpServletResponse response = (HttpServletResponse)res;
-
- String servletPath = request.getServletPath();
- String pathInfo = request.getPathInfo();
- String pathInContext = URIUtil.addPaths(servletPath, pathInfo);
-
- String resource = URIUtil.addPaths(_baseURI, pathInContext);
-
- String method = request.getMethod();
- boolean op = _operations.contains(method);
-
- if (op)
- {
- File file = null;
- try
- {
- if (method.equals(__OPTIONS))
- handleOptions(chain, request, response);
- else
- {
- file = new File(new URI(resource));
- boolean exists = file.exists();
- if (exists && !passConditionalHeaders(request, response, file))
- return;
-
- if (method.equals(__PUT))
- handlePut(request, response, pathInContext, file);
- else if (method.equals(__DELETE))
- handleDelete(request, response, pathInContext, file);
- else if (method.equals(__MOVE))
- handleMove(request, response, pathInContext, file);
- else
- throw new IllegalStateException();
- }
- }
- catch (Exception e)
- {
- _context.log(e.toString(), e);
- response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- }
- }
- else
- {
- if (isHidden(pathInContext))
- response.sendError(HttpServletResponse.SC_NOT_FOUND);
- else
- chain.doFilter(request, response);
- return;
- }
- }
-
- private boolean isHidden(String pathInContext)
- {
- return _hidden.containsKey(pathInContext);
- }
-
- @Override
- public void destroy()
- {
- }
-
- public void handlePut(HttpServletRequest request, HttpServletResponse response, String pathInContext, File file) throws ServletException, IOException
- {
- boolean exists = file.exists();
- if (pathInContext.endsWith("/"))
- {
- if (!exists)
- {
- if (!file.mkdirs())
- response.sendError(HttpServletResponse.SC_FORBIDDEN);
- else
- {
- response.setStatus(HttpServletResponse.SC_CREATED);
- response.flushBuffer();
- }
- }
- else
- {
- response.setStatus(HttpServletResponse.SC_OK);
- response.flushBuffer();
- }
- }
- else
- {
- boolean ok = false;
- try
- {
- _hidden.put(pathInContext, pathInContext);
- File parent = file.getParentFile();
- parent.mkdirs();
- int toRead = request.getContentLength();
- InputStream in = request.getInputStream();
-
- if (_putAtomic)
- {
- File tmp = File.createTempFile(file.getName(), null, _tmpdir);
- try (OutputStream out = new FileOutputStream(tmp, false))
- {
- if (toRead >= 0)
- IO.copy(in, out, toRead);
- else
- IO.copy(in, out);
- }
-
- if (!tmp.renameTo(file))
- throw new IOException("rename from " + tmp + " to " + file + " failed");
- }
- else
- {
- try (OutputStream out = new FileOutputStream(file, false))
- {
- if (toRead >= 0)
- IO.copy(in, out, toRead);
- else
- IO.copy(in, out);
- }
- }
-
- response.setStatus(exists ? HttpServletResponse.SC_OK : HttpServletResponse.SC_CREATED);
- response.flushBuffer();
- ok = true;
- }
- catch (Exception ex)
- {
- _context.log(ex.toString(), ex);
- response.sendError(HttpServletResponse.SC_FORBIDDEN);
- }
- finally
- {
- if (!ok)
- {
- try
- {
- if (file.exists())
- file.delete();
- }
- catch (Exception e)
- {
- _context.log(e.toString(), e);
- }
- }
- _hidden.remove(pathInContext);
- }
- }
- }
-
- public void handleDelete(HttpServletRequest request, HttpServletResponse response, String pathInContext, File file) throws ServletException, IOException
- {
- try
- {
- // delete the file
- if (file.delete())
- {
- response.setStatus(HttpServletResponse.SC_NO_CONTENT);
- response.flushBuffer();
- }
- else
- response.sendError(HttpServletResponse.SC_FORBIDDEN);
- }
- catch (SecurityException sex)
- {
- _context.log(sex.toString(), sex);
- response.sendError(HttpServletResponse.SC_FORBIDDEN);
- }
- }
-
- public void handleMove(HttpServletRequest request, HttpServletResponse response, String pathInContext, File file)
- throws ServletException, IOException, URISyntaxException
- {
- String newPath = URIUtil.canonicalEncodedPath(request.getHeader("new-uri"));
- if (newPath == null)
- {
- response.sendError(HttpServletResponse.SC_BAD_REQUEST);
- return;
- }
-
- String contextPath = request.getContextPath();
- if (contextPath != null && !newPath.startsWith(contextPath))
- {
- response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
- return;
- }
- String newInfo = newPath;
- if (contextPath != null)
- newInfo = newInfo.substring(contextPath.length());
-
- String newResource = URIUtil.addEncodedPaths(_baseURI, newInfo);
- File newFile = new File(new URI(newResource));
-
- file.renameTo(newFile);
-
- response.setStatus(HttpServletResponse.SC_NO_CONTENT);
- response.flushBuffer();
- }
-
- public void handleOptions(FilterChain chain, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
- {
- chain.doFilter(request, new HttpServletResponseWrapper(response)
- {
- @Override
- public void setHeader(String name, String value)
- {
- if ("Allow".equalsIgnoreCase(name))
- {
- Set options = new HashSet();
- options.addAll(Arrays.asList(StringUtil.csvSplit(value)));
- options.addAll(_operations);
- value = null;
- for (String o : options)
- {
- value = value == null ? o : (value + ", " + o);
- }
- }
-
- super.setHeader(name, value);
- }
- });
- }
-
- /*
- * Check modification date headers.
- */
- protected boolean passConditionalHeaders(HttpServletRequest request, HttpServletResponse response, File file) throws IOException
- {
- long date = 0;
-
- if ((date = request.getDateHeader("if-unmodified-since")) > 0)
- {
- if (file.lastModified() / 1000 > date / 1000)
- {
- response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
- return false;
- }
- }
-
- if ((date = request.getDateHeader("if-modified-since")) > 0)
- {
- if (file.lastModified() / 1000 <= date / 1000)
- {
- response.reset();
- response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
- response.flushBuffer();
- return false;
- }
- }
- return true;
- }
-}
diff --git a/test-proxy/src/main/java/servlets/QoSFilter.java b/test-proxy/src/main/java/servlets/QoSFilter.java
deleted file mode 100644
index 79fad50..0000000
--- a/test-proxy/src/main/java/servlets/QoSFilter.java
+++ /dev/null
@@ -1,380 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
-//
-// This program and the accompanying materials are made available under
-// the terms of the Eclipse Public License 2.0 which is available at
-// https://www.eclipse.org/legal/epl-2.0
-//
-// This Source Code may also be made available under the following
-// Secondary Licenses when the conditions for such availability set
-// forth in the Eclipse Public License, v. 2.0 are satisfied:
-// the Apache License v2.0 which is available at
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
-// ========================================================================
-//
-
-package servlets;
-
-import org.eclipse.jetty.server.handler.ContextHandler;
-import org.eclipse.jetty.util.annotation.ManagedAttribute;
-import org.eclipse.jetty.util.annotation.ManagedObject;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.servlet.*;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
-import java.io.IOException;
-import java.util.Queue;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Quality of Service Filter.
- *
- * This filter limits the number of active requests to the number set by the "maxRequests" init parameter (default 10).
- * If more requests are received, they are suspended and placed on priority queues. Priorities are determined by
- * the {@link #getPriority(ServletRequest)} method and are a value between 0 and the value given by the "maxPriority"
- * init parameter (default 10), with higher values having higher priority.
- *
- * This filter is ideal to prevent wasting threads waiting for slow/limited
- * resources such as a JDBC connection pool. It avoids the situation where all of a
- * containers thread pool may be consumed blocking on such a slow resource.
- * By limiting the number of active threads, a smaller thread pool may be used as
- * the threads are not wasted waiting. Thus more memory may be available for use by
- * the active threads.
- *
- * Furthermore, this filter uses a priority when resuming waiting requests. So that if
- * a container is under load, and there are many requests waiting for resources,
- * the {@link #getPriority(ServletRequest)} method is used, so that more important
- * requests are serviced first. For example, this filter could be deployed with a
- * maxRequest limit slightly smaller than the containers thread pool and a high priority
- * allocated to admin users. Thus regardless of load, admin users would always be
- * able to access the web application.
- *
- * The maxRequest limit is policed by a {@link Semaphore} and the filter will wait a short while attempting to acquire
- * the semaphore. This wait is controlled by the "waitMs" init parameter and allows the expense of a suspend to be
- * avoided if the semaphore is shortly available. If the semaphore cannot be obtained, the request will be suspended
- * for the default suspend period of the container or the valued set as the "suspendMs" init parameter.
- *
- * If the "managedAttr" init parameter is set to true, then this servlet is set as a {@link ServletContext} attribute with the
- * filter name as the attribute name. This allows context external mechanism (eg JMX via {@link ContextHandler#MANAGED_ATTRIBUTES}) to
- * manage the configuration of the filter.
- */
-@ManagedObject("Quality of Service Filter")
-public class QoSFilter implements Filter
-{
- private static final Logger LOG = LoggerFactory.getLogger(QoSFilter.class);
-
- static final int __DEFAULT_MAX_PRIORITY = 10;
- static final int __DEFAULT_PASSES = 10;
- static final int __DEFAULT_WAIT_MS = 50;
- static final long __DEFAULT_TIMEOUT_MS = -1;
-
- static final String MANAGED_ATTR_INIT_PARAM = "managedAttr";
- static final String MAX_REQUESTS_INIT_PARAM = "maxRequests";
- static final String MAX_PRIORITY_INIT_PARAM = "maxPriority";
- static final String MAX_WAIT_INIT_PARAM = "waitMs";
- static final String SUSPEND_INIT_PARAM = "suspendMs";
-
- private final String _suspended = "QoSFilter@" + Integer.toHexString(hashCode()) + ".SUSPENDED";
- private final String _resumed = "QoSFilter@" + Integer.toHexString(hashCode()) + ".RESUMED";
- private long _waitMs;
- private long _suspendMs;
- private int _maxRequests;
- private Semaphore _passes;
- private Queue[] _queues;
- private AsyncListener[] _listeners;
-
- @Override
- public void init(FilterConfig filterConfig)
- {
- int maxPriority = __DEFAULT_MAX_PRIORITY;
- if (filterConfig.getInitParameter(MAX_PRIORITY_INIT_PARAM) != null)
- maxPriority = Integer.parseInt(filterConfig.getInitParameter(MAX_PRIORITY_INIT_PARAM));
- _queues = new Queue[maxPriority + 1];
- _listeners = new AsyncListener[_queues.length];
- for (int p = 0; p < _queues.length; ++p)
- {
- _queues[p] = new ConcurrentLinkedQueue<>();
- _listeners[p] = new QoSAsyncListener(p);
- }
-
- int maxRequests = __DEFAULT_PASSES;
- if (filterConfig.getInitParameter(MAX_REQUESTS_INIT_PARAM) != null)
- maxRequests = Integer.parseInt(filterConfig.getInitParameter(MAX_REQUESTS_INIT_PARAM));
- _passes = new Semaphore(maxRequests, true);
- _maxRequests = maxRequests;
-
- long wait = __DEFAULT_WAIT_MS;
- if (filterConfig.getInitParameter(MAX_WAIT_INIT_PARAM) != null)
- wait = Integer.parseInt(filterConfig.getInitParameter(MAX_WAIT_INIT_PARAM));
- _waitMs = wait;
-
- long suspend = __DEFAULT_TIMEOUT_MS;
- if (filterConfig.getInitParameter(SUSPEND_INIT_PARAM) != null)
- suspend = Integer.parseInt(filterConfig.getInitParameter(SUSPEND_INIT_PARAM));
- _suspendMs = suspend;
-
- ServletContext context = filterConfig.getServletContext();
- if (context != null && Boolean.parseBoolean(filterConfig.getInitParameter(MANAGED_ATTR_INIT_PARAM)))
- context.setAttribute(filterConfig.getFilterName(), this);
- }
-
- @Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
- {
- boolean accepted = false;
- try
- {
- Boolean suspended = (Boolean)request.getAttribute(_suspended);
- if (suspended == null)
- {
- accepted = _passes.tryAcquire(getWaitMs(), TimeUnit.MILLISECONDS);
- if (accepted)
- {
- request.setAttribute(_suspended, Boolean.FALSE);
- if (LOG.isDebugEnabled())
- LOG.debug("Accepted {}", request);
- }
- else
- {
- request.setAttribute(_suspended, Boolean.TRUE);
- int priority = getPriority(request);
- AsyncContext asyncContext = request.startAsync();
- long suspendMs = getSuspendMs();
- if (suspendMs > 0)
- asyncContext.setTimeout(suspendMs);
- asyncContext.addListener(_listeners[priority]);
- _queues[priority].add(asyncContext);
- if (LOG.isDebugEnabled())
- LOG.debug("Suspended {}", request);
- return;
- }
- }
- else
- {
- if (suspended)
- {
- request.setAttribute(_suspended, Boolean.FALSE);
- Boolean resumed = (Boolean)request.getAttribute(_resumed);
- if (Boolean.TRUE.equals(resumed))
- {
- _passes.acquire();
- accepted = true;
- if (LOG.isDebugEnabled())
- LOG.debug("Resumed {}", request);
- }
- else
- {
- // Timeout! try 1 more time.
- accepted = _passes.tryAcquire(getWaitMs(), TimeUnit.MILLISECONDS);
- if (LOG.isDebugEnabled())
- LOG.debug("Timeout {}", request);
- }
- }
- else
- {
- // Pass through resume of previously accepted request.
- _passes.acquire();
- accepted = true;
- if (LOG.isDebugEnabled())
- LOG.debug("Passthrough {}", request);
- }
- }
-
- if (accepted)
- {
- chain.doFilter(request, response);
- }
- else
- {
- if (LOG.isDebugEnabled())
- LOG.debug("Rejected {}", request);
- ((HttpServletResponse)response).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
- }
- }
- catch (InterruptedException e)
- {
- ((HttpServletResponse)response).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
- }
- finally
- {
- if (accepted)
- {
- _passes.release();
-
- for (int p = _queues.length - 1; p >= 0; --p)
- {
- AsyncContext asyncContext = _queues[p].poll();
- if (asyncContext != null)
- {
- ServletRequest candidate = asyncContext.getRequest();
- Boolean suspended = (Boolean)candidate.getAttribute(_suspended);
- if (Boolean.TRUE.equals(suspended))
- {
- try
- {
- candidate.setAttribute(_resumed, Boolean.TRUE);
- asyncContext.dispatch();
- break;
- }
- catch (IllegalStateException x)
- {
- LOG.warn("Unable to resume suspended dispatch", x);
- continue;
- }
- }
- }
- }
- }
- }
- }
-
- /**
- * Computes the request priority.
- *
- * The default implementation assigns the following priorities:
- *
- * - 2 - for an authenticated request
- *
- 1 - for a request with valid / non new session
- *
- 0 - for all other requests.
- *
- * This method may be overridden to provide application specific priorities.
- *
- * @param request the incoming request
- * @return the computed request priority
- */
- protected int getPriority(ServletRequest request)
- {
- HttpServletRequest baseRequest = (HttpServletRequest)request;
- if (baseRequest.getUserPrincipal() != null)
- {
- return 2;
- }
- else
- {
- HttpSession session = baseRequest.getSession(false);
- if (session != null && !session.isNew())
- return 1;
- else
- return 0;
- }
- }
-
- @Override
- public void destroy()
- {
- }
-
- /**
- * Get the (short) amount of time (in milliseconds) that the filter would wait
- * for the semaphore to become available before suspending a request.
- *
- * @return wait time (in milliseconds)
- */
- @ManagedAttribute("(short) amount of time filter will wait before suspending request (in ms)")
- public long getWaitMs()
- {
- return _waitMs;
- }
-
- /**
- * Set the (short) amount of time (in milliseconds) that the filter would wait
- * for the semaphore to become available before suspending a request.
- *
- * @param value wait time (in milliseconds)
- */
- public void setWaitMs(long value)
- {
- _waitMs = value;
- }
-
- /**
- * Get the amount of time (in milliseconds) that the filter would suspend
- * a request for while waiting for the semaphore to become available.
- *
- * @return suspend time (in milliseconds)
- */
- @ManagedAttribute("amount of time filter will suspend a request for while waiting for the semaphore to become available (in ms)")
- public long getSuspendMs()
- {
- return _suspendMs;
- }
-
- /**
- * Set the amount of time (in milliseconds) that the filter would suspend
- * a request for while waiting for the semaphore to become available.
- *
- * @param value suspend time (in milliseconds)
- */
- public void setSuspendMs(long value)
- {
- _suspendMs = value;
- }
-
- /**
- * Get the maximum number of requests allowed to be processed
- * at the same time.
- *
- * @return maximum number of requests
- */
- @ManagedAttribute("maximum number of requests to allow processing of at the same time")
- public int getMaxRequests()
- {
- return _maxRequests;
- }
-
- /**
- * Set the maximum number of requests allowed to be processed
- * at the same time.
- *
- * @param value the number of requests
- */
- public void setMaxRequests(int value)
- {
- _passes = new Semaphore((value - getMaxRequests() + _passes.availablePermits()), true);
- _maxRequests = value;
- }
-
- private class QoSAsyncListener implements AsyncListener
- {
- private final int priority;
-
- public QoSAsyncListener(int priority)
- {
- this.priority = priority;
- }
-
- @Override
- public void onStartAsync(AsyncEvent event) throws IOException
- {
- }
-
- @Override
- public void onComplete(AsyncEvent event) throws IOException
- {
- }
-
- @Override
- public void onTimeout(AsyncEvent event) throws IOException
- {
- // Remove before it's redispatched, so it won't be
- // redispatched again at the end of the filtering.
- AsyncContext asyncContext = event.getAsyncContext();
- _queues[priority].remove(asyncContext);
- ((HttpServletResponse)event.getSuppliedResponse()).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
- asyncContext.complete();
- }
-
- @Override
- public void onError(AsyncEvent event) throws IOException
- {
- }
- }
-}
diff --git a/test-proxy/src/main/java/servlets/WelcomeFilter.java b/test-proxy/src/main/java/servlets/WelcomeFilter.java
deleted file mode 100644
index 03f325b..0000000
--- a/test-proxy/src/main/java/servlets/WelcomeFilter.java
+++ /dev/null
@@ -1,69 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
-//
-// This program and the accompanying materials are made available under
-// the terms of the Eclipse Public License 2.0 which is available at
-// https://www.eclipse.org/legal/epl-2.0
-//
-// This Source Code may also be made available under the following
-// Secondary Licenses when the conditions for such availability set
-// forth in the Eclipse Public License, v. 2.0 are satisfied:
-// the Apache License v2.0 which is available at
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
-// ========================================================================
-//
-
-package servlets;
-
-import javax.servlet.*;
-import javax.servlet.http.HttpServletRequest;
-import java.io.IOException;
-
-/**
- * Welcome Filter
- * This filter can be used to server an index file for a directory
- * when no index file actually exists (thus the web.xml mechanism does
- * not work).
- *
- * This filter will dispatch requests to a directory (URLs ending with /)
- * to the welcome URL determined by the "welcome" init parameter. So if
- * the filter "welcome" init parameter is set to "index.do" then a request
- * to "/some/directory/" will be dispatched to "/some/directory/index.do" and
- * will be handled by any servlets mapped to that URL.
- *
- * Requests to "/some/directory" will be redirected to "/some/directory/".
- */
-public class WelcomeFilter implements Filter
-{
- private String welcome;
-
- @Override
- public void init(FilterConfig filterConfig)
- {
- welcome = filterConfig.getInitParameter("welcome");
- if (welcome == null)
- welcome = "index.html";
- }
-
- @Override
- public void doFilter(ServletRequest request,
- ServletResponse response,
- FilterChain chain)
- throws IOException, ServletException
- {
- String path = ((HttpServletRequest)request).getServletPath();
- if (welcome != null && path.endsWith("/"))
- request.getRequestDispatcher(path + welcome).forward(request, response);
- else
- chain.doFilter(request, response);
- }
-
- @Override
- public void destroy()
- {
- }
-}
-
diff --git a/test-proxy/src/main/java/servlets/package-info.java b/test-proxy/src/main/java/servlets/package-info.java
deleted file mode 100644
index e9151f2..0000000
--- a/test-proxy/src/main/java/servlets/package-info.java
+++ /dev/null
@@ -1,23 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
-//
-// This program and the accompanying materials are made available under
-// the terms of the Eclipse Public License 2.0 which is available at
-// https://www.eclipse.org/legal/epl-2.0
-//
-// This Source Code may also be made available under the following
-// Secondary Licenses when the conditions for such availability set
-// forth in the Eclipse Public License, v. 2.0 are satisfied:
-// the Apache License v2.0 which is available at
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
-// ========================================================================
-//
-
-/**
- * Jetty Servlets : Generally Useful Servlets, Handlers and Filters
- */
-package servlets;
-
diff --git a/test-proxy/src/main/webapp/WEB-INF/web.xml b/test-proxy/src/main/webapp/WEB-INF/web.xml
index 9f88c1f..dcc0494 100644
--- a/test-proxy/src/main/webapp/WEB-INF/web.xml
+++ b/test-proxy/src/main/webapp/WEB-INF/web.xml
@@ -1,7 +1,22 @@
+ "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
+ "http://java.sun.com/dtd/web-app_2_3.dtd" >
+
+
+
+ misp-proxy
+
+
+ test-proxy
+ com.olexyn.test.proxy.ProxyServlet
+
+
+
+ test-proxy
+ /core
+
-
- Archetype Created Web Application
diff --git a/test-proxy/src/main/webapp/index.jsp b/test-proxy/src/main/webapp/index.jsp
index c38169b..1ed1b19 100644
--- a/test-proxy/src/main/webapp/index.jsp
+++ b/test-proxy/src/main/webapp/index.jsp
@@ -1,5 +1,20 @@
-
-Hello World!
+
+test-proxy
+
+
+
+
+
+
+
+ |
+
+
+
+test-proxy
+ |
+
+