DosGuardFilter.java
/*
* Copyright (c) 2002-2022, City of Paris
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright notice
* and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice
* and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* License 1.0
*/
package fr.paris.lutece.portal.web.upload;
import fr.paris.lutece.portal.service.util.AppLogService;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.commons.collections.CollectionUtils;
/**
* A rewrite of the multipart filter from the com.oreilly.servlet package. The rewrite allows us to use initialization parameters specified in the Lutece
* configuration files.
*/
public class DosGuardFilter implements Filter
{
// Initial capacity of the HashMap
private static final int INITIAL_CAPACITY = 100;
// The size under which requests are allowed systematically
private int _nMinContentLength;
// The minimum interval allowed between two requests from the same client
private int _nMinInterval;
// The HashMap used to store IP/time entries
private Map<String, Long> _mapLastRequestTimes;
// The LinkedList used to store entries in their order of arrival (to speed
// up cleaning the HashMap)
private LinkedList<Entry> _listOrderedRequests;
/**
* {@inheritDoc}
*/
@Override
public void init( FilterConfig config ) throws ServletException
{
_mapLastRequestTimes = new HashMap<>( INITIAL_CAPACITY );
_listOrderedRequests = new LinkedList<>( );
try
{
String paramValue = config.getInitParameter( "minContentLength" );
if ( paramValue != null )
{
_nMinContentLength = Integer.parseInt( paramValue );
}
paramValue = config.getInitParameter( "minInterval" );
if ( paramValue != null )
{
_nMinInterval = Integer.parseInt( paramValue );
}
}
catch( NumberFormatException ex )
{
ServletException servletEx = new ServletException( ex.getMessage( ) );
servletEx.initCause( ex );
throw servletEx;
}
}
/**
* {@inheritDoc}
*/
@Override
public void destroy( )
{
// Do nothing
}
/**
* {@inheritDoc}
*/
@Override
public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain ) throws IOException, ServletException
{
// DOS check
if ( this.isAllowed( request.getRemoteAddr( ), request.getContentLength( ) ) )
{
chain.doFilter( request, response );
}
else
{
throw new ServletException( "DOS Guard : Too many upload from the same IP !" );
}
}
/**
* Checks if a client is allowed to make a request at the present time.
*
* @param strRemoteAddr
* the IP address of the client
* @param iContentLength
* the size of the request
* @return true if allowed, false otherwize
*/
public synchronized boolean isAllowed( String strRemoteAddr, int iContentLength )
{
AppLogService.debug( "DosGuard : isAllowed({}, {})", strRemoteAddr, iContentLength );
// Ignore requests if minInterval is negative (e.g. -1)
if ( _nMinInterval < 0 )
{
AppLogService.debug( "minInterval is below minimum, ignored" );
return true;
}
// Ignore the requests under the minimum size
if ( iContentLength < _nMinContentLength )
{
AppLogService.debug( "ContentLength is below minimum, ignored" );
return true;
}
// Record the time of this request
long lRequestTime = System.currentTimeMillis( );
AppLogService.debug( "Request time : {}", lRequestTime );
// Test if IP was previously recorded
Long previousRequestTime = _mapLastRequestTimes.get( strRemoteAddr );
AppLogService.debug( "Previous request time : {}", previousRequestTime );
if ( previousRequestTime != null )
{
AppLogService.debug( "IP is in the map" );
// Test if IP is allowed to make a new request
if ( lRequestTime > ( previousRequestTime.longValue( ) + _nMinInterval ) )
{
AppLogService.debug( "IP is allowed to make a new request" );
// Clean up
this.cleanExpiredEntries( );
// Update the map with the new time
_mapLastRequestTimes.put( strRemoteAddr, Long.valueOf( lRequestTime ) );
// Add a new entry in the list
_listOrderedRequests.addFirst( new Entry( strRemoteAddr, lRequestTime ) );
return true;
}
AppLogService.debug( "IP is not allowed to make a new request" );
return false;
}
AppLogService.debug( "IP is not in the map" );
// Clean up
this.cleanExpiredEntries( );
// Add the IP and the time to the map
_mapLastRequestTimes.put( strRemoteAddr, Long.valueOf( lRequestTime ) );
// Add a new entry in the list
_listOrderedRequests.addFirst( new Entry( strRemoteAddr, lRequestTime ) );
return true;
}
/**
* Cleans the internal map from expired entries.
*/
private void cleanExpiredEntries( )
{
AppLogService.debug( "DosGuard.class : cleanExpiredEntries()" );
if ( CollectionUtils.isNotEmpty( _listOrderedRequests ) )
{
// Expired entries are those where the IP can't be blocked anymore
long lMinTime = System.currentTimeMillis( ) - _nMinInterval;
AppLogService.debug( "Min time : {}", lMinTime );
// Read entries from the list, remove them as long as they are expired
boolean bDone = false;
while ( !bDone && CollectionUtils.isNotEmpty( _listOrderedRequests ) )
{
// The list is ordered by arrival time, so the last one is the
// oldest
Entry lastEntry = _listOrderedRequests.getLast( );
if ( lastEntry.getRequestTime( ) < lMinTime )
{
// The entry is expired, remove it from the map and the list
_mapLastRequestTimes.remove( lastEntry.getRemoteAddr( ) );
_listOrderedRequests.removeLast( );
AppLogService.debug( "Removing [{}, {}]", lastEntry.getRemoteAddr( ), lastEntry.getRequestTime( ) );
}
else
{
bDone = true;
}
}
}
}
/**
* Utility class used to store entries in the list.
*/
private static class Entry
{
private String _strRemoteAddr;
private long _lRequestTime;
/**
* Constructor
*
* @param strRemoteAddr
* The remote address
* @param lRequestTime
* The request time
*/
public Entry( String strRemoteAddr, long lRequestTime )
{
this._strRemoteAddr = strRemoteAddr;
this._lRequestTime = lRequestTime;
}
/**
* Gets the remote address
*
* @return The remote address
*/
public String getRemoteAddr( )
{
return _strRemoteAddr;
}
/**
* Gets the request time
*
* @return The request time
*/
public long getRequestTime( )
{
return _lRequestTime;
}
}
}