View Javadoc
1   /*
2    * Copyright (c) 2002-2014, Mairie de 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  
52  /**
53   * A rewrite of the multipart filter from the com.oreilly.servlet package. The
54   * 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      private FilterConfig _filterConfig;
62  
63      // The size under which requests are allowed systematically
64      private int _nMinContentLength;
65  
66      // The minimum interval allowed between two requests from the same client
67      private int _nMinInterval;
68  
69      // The HashMap used to store IP/time entries
70      private Map<String, Long> _mapLastRequestTimes;
71  
72      // The LinkedList used to store entries in their order of arrival (to speed
73      // up cleaning the HashMap)
74      private LinkedList<Entry> _listOrderedRequests;
75  
76      /**
77       * {@inheritDoc}
78       */
79      @Override
80      public void init( FilterConfig config ) throws ServletException
81      {
82          _filterConfig = config;
83          _mapLastRequestTimes = new HashMap<String, Long>( INITIAL_CAPACITY );
84          _listOrderedRequests = new LinkedList<Entry>(  );
85  
86          try
87          {
88              String paramValue = _filterConfig.getInitParameter( "minContentLength" );
89  
90              if ( paramValue != null )
91              {
92                  _nMinContentLength = Integer.parseInt( paramValue );
93              }
94  
95              paramValue = _filterConfig.getInitParameter( "minInterval" );
96  
97              if ( paramValue != null )
98              {
99                  _nMinInterval = Integer.parseInt( paramValue );
100             }
101         }
102         catch ( NumberFormatException ex )
103         {
104             ServletException servletEx = new ServletException( ex.getMessage(  ) );
105             servletEx.initCause( ex );
106             throw servletEx;
107         }
108     }
109 
110     /**
111      * {@inheritDoc}
112      */
113     @Override
114     public void destroy(  )
115     {
116         // Do nothing
117     }
118 
119     /**
120      * {@inheritDoc}
121      */
122     @Override
123     public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain )
124         throws IOException, ServletException
125     {
126         // DOS check
127         if ( this.isAllowed( request.getRemoteAddr(  ), request.getContentLength(  ) ) )
128         {
129             chain.doFilter( request, response );
130         }
131         else
132         {
133             throw new ServletException( "DOS Guard : Too many upload from the same IP !" );
134         }
135     }
136 
137     /**
138      * Checks if a client is allowed to make a request at the present time.
139      *
140      * @param strRemoteAddr the IP address of the client
141      * @param iContentLength the size of the request
142      * @return true if allowed, false otherwize
143      */
144     public synchronized boolean isAllowed( String strRemoteAddr, int iContentLength )
145     {
146         AppLogService.debug( "DosGuard : isAllowed(" + strRemoteAddr + ", " + iContentLength + ")" );
147 
148         // Ignore the requests under the minimum size
149         if ( iContentLength < _nMinContentLength )
150         {
151             AppLogService.debug( "ContentLength is below minimum, ignored" );
152 
153             return true;
154         }
155 
156         // Record the time of this request
157         long lRequestTime = System.currentTimeMillis(  );
158         AppLogService.debug( "Request time : " + lRequestTime );
159 
160         // Test if IP was previously recorded
161         Long previousRequestTime = _mapLastRequestTimes.get( strRemoteAddr );
162         AppLogService.debug( "Previous request time : " + previousRequestTime );
163 
164         if ( previousRequestTime != null )
165         {
166             AppLogService.debug( "IP is in the map" );
167 
168             // Test if IP is allowed to make a new request
169             if ( lRequestTime > ( previousRequestTime.longValue(  ) + _nMinInterval ) )
170             {
171                 AppLogService.debug( "IP is allowed to make a new request" );
172 
173                 // Clean up
174                 this.cleanExpiredEntries(  );
175 
176                 // Update the map with the new time
177                 _mapLastRequestTimes.put( strRemoteAddr, Long.valueOf( lRequestTime ) );
178 
179                 // Add a new entry in the list
180                 _listOrderedRequests.addFirst( new Entry( strRemoteAddr, lRequestTime ) );
181 
182                 return true;
183             }
184 
185             AppLogService.debug( "IP is not allowed to make a new request" );
186 
187             return false;
188         }
189 
190         AppLogService.debug( "IP is not in the map" );
191 
192         // Clean up
193         this.cleanExpiredEntries(  );
194 
195         // Add the IP and the time to the map
196         _mapLastRequestTimes.put( strRemoteAddr, Long.valueOf( lRequestTime ) );
197 
198         // Add a new entry in the list
199         _listOrderedRequests.addFirst( new Entry( strRemoteAddr, lRequestTime ) );
200 
201         return true;
202     }
203 
204     /**
205      * Cleans the internal map from expired entries.
206      */
207     private void cleanExpiredEntries(  )
208     {
209         AppLogService.debug( "DosGuard.class : cleanExpiredEntries()" );
210 
211         if ( _listOrderedRequests.size(  ) != 0 )
212         {
213             // Expired entries are those where the IP can't be blocked anymore
214             long lMinTime = System.currentTimeMillis(  ) - _nMinInterval;
215 
216             AppLogService.debug( "Min time : " + lMinTime );
217 
218             // Read entries from the list, remove them as long as they are expired
219             boolean bDone = false;
220 
221             while ( !bDone && ( _listOrderedRequests.size(  ) > 0 ) )
222             {
223                 // The list is ordered by arrival time, so the last one is the
224                 // oldest
225                 Entry lastEntry = _listOrderedRequests.getLast(  );
226 
227                 if ( lastEntry.getRequestTime(  ) < lMinTime )
228                 {
229                     // The entry is expired, remove it from the map and the list
230                     _mapLastRequestTimes.remove( lastEntry.getRemoteAddr(  ) );
231                     _listOrderedRequests.removeLast(  );
232 
233                     AppLogService.debug( "Removing [" + lastEntry.getRemoteAddr(  ) + ", " +
234                         lastEntry.getRequestTime(  ) + "]" );
235                 }
236                 else
237                 {
238                     bDone = true;
239                 }
240             }
241         }
242     }
243 
244     /**
245      * Utility class used to store entries in the list.
246      */
247     private static class Entry
248     {
249         private String _strRemoteAddr;
250         private long _lRequestTime;
251 
252         /**
253          * Constructor
254          * @param strRemoteAddr The remote address
255          * @param lRequestTime The request time
256          */
257         public Entry( String strRemoteAddr, long lRequestTime )
258         {
259             this._strRemoteAddr = strRemoteAddr;
260             this._lRequestTime = lRequestTime;
261         }
262 
263         /**
264          * Gets the remote address
265          * @return The remote address
266          */
267         public String getRemoteAddr(  )
268         {
269             return _strRemoteAddr;
270         }
271 
272         /**
273          * Gets the request time
274          * @return The request time
275          */
276         public long getRequestTime(  )
277         {
278             return _lRequestTime;
279         }
280     }
281 }