View Javadoc
1   /*
2    * Copyright (c) 2002-2022, City of Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.portal.web.upload;
35  
36  import fr.paris.lutece.portal.service.util.AppLogService;
37  
38  import java.io.IOException;
39  
40  import java.util.HashMap;
41  import java.util.LinkedList;
42  import java.util.Map;
43  
44  import javax.servlet.Filter;
45  import javax.servlet.FilterChain;
46  import javax.servlet.FilterConfig;
47  import javax.servlet.ServletException;
48  import javax.servlet.ServletRequest;
49  import javax.servlet.ServletResponse;
50  
51  import org.apache.commons.collections.CollectionUtils;
52  
53  /**
54   * A rewrite of the multipart filter from the com.oreilly.servlet package. The rewrite allows us to use initialization parameters specified in the Lutece
55   * configuration files.
56   */
57  public class DosGuardFilter implements Filter
58  {
59      // Initial capacity of the HashMap
60      private static final int INITIAL_CAPACITY = 100;
61  
62      // The size under which requests are allowed systematically
63      private int _nMinContentLength;
64  
65      // The minimum interval allowed between two requests from the same client
66      private int _nMinInterval;
67  
68      // The HashMap used to store IP/time entries
69      private Map<String, Long> _mapLastRequestTimes;
70  
71      // The LinkedList used to store entries in their order of arrival (to speed
72      // up cleaning the HashMap)
73      private LinkedList<Entry> _listOrderedRequests;
74  
75      /**
76       * {@inheritDoc}
77       */
78      @Override
79      public void init( FilterConfig config ) throws ServletException
80      {
81          _mapLastRequestTimes = new HashMap<>( INITIAL_CAPACITY );
82          _listOrderedRequests = new LinkedList<>( );
83  
84          try
85          {
86              String paramValue = config.getInitParameter( "minContentLength" );
87  
88              if ( paramValue != null )
89              {
90                  _nMinContentLength = Integer.parseInt( paramValue );
91              }
92  
93              paramValue = config.getInitParameter( "minInterval" );
94  
95              if ( paramValue != null )
96              {
97                  _nMinInterval = Integer.parseInt( paramValue );
98              }
99          }
100         catch( NumberFormatException ex )
101         {
102             ServletException servletEx = new ServletException( ex.getMessage( ) );
103             servletEx.initCause( ex );
104             throw servletEx;
105         }
106     }
107 
108     /**
109      * {@inheritDoc}
110      */
111     @Override
112     public void destroy( )
113     {
114         // Do nothing
115     }
116 
117     /**
118      * {@inheritDoc}
119      */
120     @Override
121     public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain ) throws IOException, ServletException
122     {
123         // DOS check
124         if ( this.isAllowed( request.getRemoteAddr( ), request.getContentLength( ) ) )
125         {
126             chain.doFilter( request, response );
127         }
128         else
129         {
130             throw new ServletException( "DOS Guard : Too many upload from the same IP !" );
131         }
132     }
133 
134     /**
135      * Checks if a client is allowed to make a request at the present time.
136      *
137      * @param strRemoteAddr
138      *            the IP address of the client
139      * @param iContentLength
140      *            the size of the request
141      * @return true if allowed, false otherwize
142      */
143     public synchronized boolean isAllowed( String strRemoteAddr, int iContentLength )
144     {
145         AppLogService.debug( "DosGuard : isAllowed({}, {})", strRemoteAddr, iContentLength );
146 
147         // Ignore requests if minInterval is negative (e.g. -1)
148         if ( _nMinInterval < 0 )
149         {
150             AppLogService.debug( "minInterval is below minimum, ignored" );
151 
152             return true;
153         }
154 
155         // Ignore the requests under the minimum size
156         if ( iContentLength < _nMinContentLength )
157         {
158             AppLogService.debug( "ContentLength is below minimum, ignored" );
159 
160             return true;
161         }
162 
163         // Record the time of this request
164         long lRequestTime = System.currentTimeMillis( );
165         AppLogService.debug( "Request time : {}", lRequestTime );
166 
167         // Test if IP was previously recorded
168         Long previousRequestTime = _mapLastRequestTimes.get( strRemoteAddr );
169         AppLogService.debug( "Previous request time : {}", previousRequestTime );
170 
171         if ( previousRequestTime != null )
172         {
173             AppLogService.debug( "IP is in the map" );
174 
175             // Test if IP is allowed to make a new request
176             if ( lRequestTime > ( previousRequestTime.longValue( ) + _nMinInterval ) )
177             {
178                 AppLogService.debug( "IP is allowed to make a new request" );
179 
180                 // Clean up
181                 this.cleanExpiredEntries( );
182 
183                 // Update the map with the new time
184                 _mapLastRequestTimes.put( strRemoteAddr, Long.valueOf( lRequestTime ) );
185 
186                 // Add a new entry in the list
187                 _listOrderedRequests.addFirst( new Entry( strRemoteAddr, lRequestTime ) );
188 
189                 return true;
190             }
191 
192             AppLogService.debug( "IP is not allowed to make a new request" );
193 
194             return false;
195         }
196 
197         AppLogService.debug( "IP is not in the map" );
198 
199         // Clean up
200         this.cleanExpiredEntries( );
201 
202         // Add the IP and the time to the map
203         _mapLastRequestTimes.put( strRemoteAddr, Long.valueOf( lRequestTime ) );
204 
205         // Add a new entry in the list
206         _listOrderedRequests.addFirst( new Entry( strRemoteAddr, lRequestTime ) );
207 
208         return true;
209     }
210 
211     /**
212      * Cleans the internal map from expired entries.
213      */
214     private void cleanExpiredEntries( )
215     {
216         AppLogService.debug( "DosGuard.class : cleanExpiredEntries()" );
217 
218         if ( CollectionUtils.isNotEmpty( _listOrderedRequests ) )
219         {
220             // Expired entries are those where the IP can't be blocked anymore
221             long lMinTime = System.currentTimeMillis( ) - _nMinInterval;
222 
223             AppLogService.debug( "Min time : {}", lMinTime );
224 
225             // Read entries from the list, remove them as long as they are expired
226             boolean bDone = false;
227 
228             while ( !bDone && CollectionUtils.isNotEmpty( _listOrderedRequests ) )
229             {
230                 // The list is ordered by arrival time, so the last one is the
231                 // oldest
232                 Entry lastEntry = _listOrderedRequests.getLast( );
233 
234                 if ( lastEntry.getRequestTime( ) < lMinTime )
235                 {
236                     // The entry is expired, remove it from the map and the list
237                     _mapLastRequestTimes.remove( lastEntry.getRemoteAddr( ) );
238                     _listOrderedRequests.removeLast( );
239 
240                     AppLogService.debug( "Removing [{}, {}]", lastEntry.getRemoteAddr( ), lastEntry.getRequestTime( ) );
241                 }
242                 else
243                 {
244                     bDone = true;
245                 }
246             }
247         }
248     }
249 
250     /**
251      * Utility class used to store entries in the list.
252      */
253     private static class Entry
254     {
255         private String _strRemoteAddr;
256         private long _lRequestTime;
257 
258         /**
259          * Constructor
260          * 
261          * @param strRemoteAddr
262          *            The remote address
263          * @param lRequestTime
264          *            The request time
265          */
266         public Entry( String strRemoteAddr, long lRequestTime )
267         {
268             this._strRemoteAddr = strRemoteAddr;
269             this._lRequestTime = lRequestTime;
270         }
271 
272         /**
273          * Gets the remote address
274          * 
275          * @return The remote address
276          */
277         public String getRemoteAddr( )
278         {
279             return _strRemoteAddr;
280         }
281 
282         /**
283          * Gets the request time
284          * 
285          * @return The request time
286          */
287         public long getRequestTime( )
288         {
289             return _lRequestTime;
290         }
291     }
292 }