Revision: 8837
          
http://languagetool.svn.sourceforge.net/languagetool/?rev=8837&view=rev
Author:   dnaber
Date:     2013-01-04 12:08:52 +0000 (Fri, 04 Jan 2013)
Log Message:
-----------
embedded HTTPS server: allow limiting the maximum number of requests per IP in 
a given time period

Modified Paths:
--------------
    trunk/JLanguageTool/CHANGES.txt
    trunk/JLanguageTool/src/main/java/org/languagetool/server/HTTPSServer.java
    
trunk/JLanguageTool/src/main/java/org/languagetool/server/HTTPSServerConfig.java
    trunk/JLanguageTool/src/main/java/org/languagetool/server/HTTPServer.java
    
trunk/JLanguageTool/src/main/java/org/languagetool/server/LanguageToolHttpHandler.java
    
trunk/JLanguageTool/src/test/java/org/languagetool/server/HTTPSServerTest.java
    
trunk/JLanguageTool/src/test/java/org/languagetool/server/HTTPServerTest.java

Added Paths:
-----------
    
trunk/JLanguageTool/src/main/java/org/languagetool/server/RequestLimiter.java
    
trunk/JLanguageTool/src/test/java/org/languagetool/server/RequestLimiterTest.java

Modified: trunk/JLanguageTool/CHANGES.txt
===================================================================
--- trunk/JLanguageTool/CHANGES.txt     2013-01-04 08:20:43 UTC (rev 8836)
+++ trunk/JLanguageTool/CHANGES.txt     2013-01-04 12:08:52 UTC (rev 8837)
@@ -29,6 +29,11 @@
 
  -stand-alone GUI: the very first check for languages with a lot of rules
   (e.g. German, French) should now be faster
+  
+ -embedded HTTPS server: two new properties, to be set from the property 
configuration file,
+  allow limiting the maximum number of requests:
+    requestLimit - the maximum number of requests 
+    requestLimitPeriodInSeconds - the time period in which the requests are 
considered, in seconds
 
 
 2.0 (2012-12-30)

Modified: 
trunk/JLanguageTool/src/main/java/org/languagetool/server/HTTPSServer.java
===================================================================
--- trunk/JLanguageTool/src/main/java/org/languagetool/server/HTTPSServer.java  
2013-01-04 08:20:43 UTC (rev 8836)
+++ trunk/JLanguageTool/src/main/java/org/languagetool/server/HTTPSServer.java  
2013-01-04 12:08:52 UTC (rev 8837)
@@ -66,7 +66,8 @@
       final SSLContext sslContext = getSslContext(config.getKeystore(), 
config.getKeyStorePassword());
       final HttpsConfigurator configurator = getConfigurator(sslContext);
       ((HttpsServer)server).setHttpsConfigurator(configurator);
-      final LanguageToolHttpHandler httpHandler = new 
LanguageToolHttpHandler(config.isVerbose(), allowedIps, runInternally);
+      final RequestLimiter limiter = getRequestLimiterOrNull(config);
+      final LanguageToolHttpHandler httpHandler = new 
LanguageToolHttpHandler(config.isVerbose(), allowedIps, runInternally, limiter);
       httpHandler.setMaxTextLength(config.getMaxTextLength());
       server.createContext("/", httpHandler);
     } catch (BindException e) {
@@ -80,6 +81,15 @@
     }
   }
 
+  private RequestLimiter getRequestLimiterOrNull(HTTPSServerConfig config) {
+    final int requestLimit = config.getRequestLimit();
+    final int requestLimitPeriodInSeconds = 
config.getRequestLimitPeriodInSeconds();
+    if (requestLimit > 0 || requestLimitPeriodInSeconds > 0) {
+      return new RequestLimiter(requestLimit, requestLimitPeriodInSeconds);
+    }
+    return null;
+  }
+
   private SSLContext getSslContext(File keyStoreFile, String passPhrase) 
throws IOException {
     final FileInputStream keyStoreStream = new FileInputStream(keyStoreFile);
     try {

Modified: 
trunk/JLanguageTool/src/main/java/org/languagetool/server/HTTPSServerConfig.java
===================================================================
--- 
trunk/JLanguageTool/src/main/java/org/languagetool/server/HTTPSServerConfig.java
    2013-01-04 08:20:43 UTC (rev 8836)
+++ 
trunk/JLanguageTool/src/main/java/org/languagetool/server/HTTPSServerConfig.java
    2013-01-04 12:08:52 UTC (rev 8837)
@@ -30,7 +30,9 @@
 
   private final File keystore;
   private final String keyStorePassword;
-
+  
+  private int requestLimit;
+  private int requestLimitPeriodInSeconds;
   private int maxTextLength = Integer.MAX_VALUE;
 
   /**
@@ -56,6 +58,21 @@
   }
 
   /**
+   * @param serverPort the port to bind to
+   * @param verbose when set to <tt>true</tt>, the input text will be logged 
in case there is an exception
+   * @param keystore a Java keystore file as created with the <tt>keytool</tt> 
command
+   * @param keyStorePassword the password for the keystore
+   * @since 2.1
+   */
+  HTTPSServerConfig(int serverPort, boolean verbose, File keystore, String 
keyStorePassword, int requestLimit, int requestLimitPeriodInSeconds) {
+    super(serverPort, verbose);
+    this.keystore = keystore;
+    this.keyStorePassword = keyStorePassword;
+    this.requestLimit = requestLimit;
+    this.requestLimitPeriodInSeconds = requestLimitPeriodInSeconds;
+  }
+
+  /**
    * Parse command line options and load settings from property file.
    */
   HTTPSServerConfig(String[] args) {
@@ -76,6 +93,8 @@
         props.load(fis);
         keystore = new File(getProperty(props, "keystore", config));
         keyStorePassword = getProperty(props, "password", config);
+        requestLimit = Integer.parseInt(getOptionalProperty(props, 
"requestLimit", "0"));
+        requestLimitPeriodInSeconds = 
Integer.parseInt(getOptionalProperty(props, "requestLimitPeriodInSeconds", 
"0"));
         maxTextLength = Integer.parseInt(getOptionalProperty(props, 
"maxTextLength", Integer.toString(Integer.MAX_VALUE)));
       } finally {
         fis.close();
@@ -105,6 +124,14 @@
     return keyStorePassword;
   }
 
+  int getRequestLimit() {
+    return requestLimit;
+  }
+
+  int getRequestLimitPeriodInSeconds() {
+    return requestLimitPeriodInSeconds;
+  }
+
   private String getProperty(Properties props, String propertyName, File 
config) {
     final String propertyValue = (String)props.get(propertyName);
     if (propertyValue == null || propertyValue.trim().isEmpty()) {

Modified: 
trunk/JLanguageTool/src/main/java/org/languagetool/server/HTTPServer.java
===================================================================
--- trunk/JLanguageTool/src/main/java/org/languagetool/server/HTTPServer.java   
2013-01-04 08:20:43 UTC (rev 8836)
+++ trunk/JLanguageTool/src/main/java/org/languagetool/server/HTTPServer.java   
2013-01-04 12:08:52 UTC (rev 8837)
@@ -28,7 +28,6 @@
 import java.util.Set;
 
 import static org.languagetool.server.HTTPServerConfig.DEFAULT_HOST;
-import static org.languagetool.server.HTTPServerConfig.DEFAULT_PORT;
 
 /**
  * A small embedded HTTP server that checks text. Returns XML, prints debugging
@@ -96,7 +95,7 @@
       } else {
         server = HttpServer.create(new InetSocketAddress(host, port), 0);
       }
-      server.createContext("/", new 
LanguageToolHttpHandler(config.isVerbose(), allowedIps, runInternally));
+      server.createContext("/", new 
LanguageToolHttpHandler(config.isVerbose(), allowedIps, runInternally, null));
     } catch (Exception e) {
       final ResourceBundle messages = JLanguageTool.getMessageBundle();
       final String message = Tools.makeTexti18n(messages, 
"http_server_start_failed", host, Integer.toString(port));

Modified: 
trunk/JLanguageTool/src/main/java/org/languagetool/server/LanguageToolHttpHandler.java
===================================================================
--- 
trunk/JLanguageTool/src/main/java/org/languagetool/server/LanguageToolHttpHandler.java
      2013-01-04 08:20:43 UTC (rev 8836)
+++ 
trunk/JLanguageTool/src/main/java/org/languagetool/server/LanguageToolHttpHandler.java
      2013-01-04 12:08:52 UTC (rev 8837)
@@ -36,6 +36,7 @@
   private final Set<String> allowedIps;  
   private final boolean verbose;
   private final boolean internalServer;
+  private final RequestLimiter requestLimiter;
   
   private Configuration config;
   
@@ -48,12 +49,14 @@
   /**
    * @param verbose print the input text in case of exceptions
    * @param allowedIps set of IPs that may connect or <tt>null</tt> to allow 
any IP
+   * @param requestLimiter may be null
    * @throws IOException
    */
-  LanguageToolHttpHandler(boolean verbose, Set<String> allowedIps, boolean 
internal) throws IOException {
+  LanguageToolHttpHandler(boolean verbose, Set<String> allowedIps, boolean 
internal, RequestLimiter requestLimiter) throws IOException {
     this.verbose = verbose;
     this.allowedIps = allowedIps;
     this.internalServer = internal;
+    this.requestLimiter = requestLimiter;
     config = new Configuration(null);
   }
 
@@ -66,8 +69,15 @@
     String text = null;
     try {
       final URI requestedUri = httpExchange.getRequestURI();
+      final String remoteAddress = 
httpExchange.getRemoteAddress().getAddress().getHostAddress();
+      if (requestLimiter != null && 
!requestLimiter.isAccessOkay(remoteAddress)) {
+        final String errorMessage = "Error: Access from " + 
StringTools.escapeXML(remoteAddress) +
+                " denied - too many requests. Allowed maximum requests: " + 
requestLimiter.getRequestLimit() +
+                " requests per " + 
requestLimiter.getRequestLimitPeriodInSeconds() + " seconds";
+        sendError(httpExchange, HttpURLConnection.HTTP_FORBIDDEN, 
errorMessage);
+        throw new RuntimeException(errorMessage);
+      }
       final Map<String, String> parameters = getRequestQuery(httpExchange, 
requestedUri);
-      final String remoteAddress = 
httpExchange.getRemoteAddress().getAddress().getHostAddress();
       if (allowedIps == null || allowedIps.contains(remoteAddress)) {
         if (requestedUri.getRawPath().endsWith("/Languages")) {
           // request type: list known languages
@@ -179,13 +189,13 @@
     final String enabledParam = parameters.get("enabled");
     enabledRules = new String[0];
     if (null != enabledParam) {
-       enabledRules = enabledParam.split(",");
+      enabledRules = enabledParam.split(",");
     }
     
     final String disabledParam = parameters.get("disabled");
     disabledRules = new String[0];
     if (null != disabledParam) {
-       disabledRules = disabledParam.split(",");
+      disabledRules = disabledParam.split(",");
     }
     
     useQuerySettings = enabledRules.length > 0 || disabledRules.length > 0; 

Added: 
trunk/JLanguageTool/src/main/java/org/languagetool/server/RequestLimiter.java
===================================================================
--- 
trunk/JLanguageTool/src/main/java/org/languagetool/server/RequestLimiter.java   
                            (rev 0)
+++ 
trunk/JLanguageTool/src/main/java/org/languagetool/server/RequestLimiter.java   
    2013-01-04 12:08:52 UTC (rev 8837)
@@ -0,0 +1,99 @@
+/* LanguageTool, a natural language style checker
+ * Copyright (C) 2012 Daniel Naber (http://www.danielnaber.de)
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
+ * USA
+ */
+package org.languagetool.server;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Limit the maximum number of request per IP address for a given time range.
+ */
+class RequestLimiter {
+
+  private static final int API_REQUEST_QUEUE_SIZE = 1000;
+
+  private final List<RequestEvent> requestEvents = new 
ArrayList<RequestEvent>();
+
+  private final int requestLimit;
+
+  private final int requestLimitPeriodInSeconds;
+
+  /**
+   * @param requestLimit the maximum number of request per 
<tt>requestLimitPeriodInSeconds</tt>
+   * @param requestLimitPeriodInSeconds the time period over which requests 
are considered, in seconds
+   */
+  RequestLimiter(int requestLimit, int requestLimitPeriodInSeconds) {
+    this.requestLimit = requestLimit;
+    this.requestLimitPeriodInSeconds = requestLimitPeriodInSeconds;
+  }
+
+  /**
+   * The maximum number of request per {@link 
#getRequestLimitPeriodInSeconds()}.
+   */
+  int getRequestLimit() {
+    return requestLimit;
+  }
+
+  /**
+   * The time period over which requests are considered, in seconds.
+   */
+  int getRequestLimitPeriodInSeconds() {
+    return requestLimitPeriodInSeconds;
+  }
+
+  /**
+   * @param ipAddress the client's IP address
+   * @return true if access is allowed because the request limit is not 
reached yet
+   */
+  boolean isAccessOkay(String ipAddress) {
+    while (requestEvents.size() > API_REQUEST_QUEUE_SIZE) {
+      requestEvents.remove(0);
+    }
+    requestEvents.add(new RequestEvent(ipAddress, new Date()));
+    return !limitReached(ipAddress);
+  }
+  
+  private boolean limitReached(String ipAddress) {
+    int requestsByIp = 0;
+    // all requests before this date are considered old:
+    final Date thresholdDate = new Date(System.currentTimeMillis() - 
requestLimitPeriodInSeconds * 1000);
+    for (RequestEvent requestEvent : requestEvents) {
+      if (requestEvent.ip.equals(ipAddress) && 
requestEvent.date.after(thresholdDate)) {
+        requestsByIp++;
+        if (requestsByIp > requestLimit) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+  
+  class RequestEvent {
+
+    private final String ip;
+    private final Date date;
+
+    RequestEvent(String ip, Date date) {
+      this.ip = ip;
+      this.date = date;
+    }
+  }
+
+}

Modified: 
trunk/JLanguageTool/src/test/java/org/languagetool/server/HTTPSServerTest.java
===================================================================
--- 
trunk/JLanguageTool/src/test/java/org/languagetool/server/HTTPSServerTest.java  
    2013-01-04 08:20:43 UTC (rev 8836)
+++ 
trunk/JLanguageTool/src/test/java/org/languagetool/server/HTTPSServerTest.java  
    2013-01-04 12:08:52 UTC (rev 8837)
@@ -19,6 +19,7 @@
 package org.languagetool.server;
 
 import org.junit.Test;
+import org.languagetool.Language;
 
 import java.io.File;
 import java.io.IOException;
@@ -29,6 +30,7 @@
 
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.languagetool.server.HTTPServerConfig.DEFAULT_PORT;
 
 public class HTTPSServerTest {
 
@@ -36,14 +38,28 @@
   private static final String KEYSTORE_PASSWORD = "mytest";
 
   @Test
+  public void runRequestLimitationTest() throws Exception {
+    HTTPTools.disableCertChecks();
+    final HTTPSServerConfig serverConfig = new 
HTTPSServerConfig(HTTPServerConfig.DEFAULT_PORT, false, getKeystoreFile(), 
KEYSTORE_PASSWORD, 2, 30);
+    final HTTPSServer server = new HTTPSServer(serverConfig, false, 
HTTPServerConfig.DEFAULT_HOST, null);
+    try {
+      server.run();
+      check(Language.GERMAN, "foo");
+      check(Language.GERMAN, "foo");
+      try {
+        System.out.println("Testing too many requests now, please ignore the 
exception");
+        check(Language.GERMAN, "foo");
+        fail();
+      } catch (IOException expected) {}
+    } finally {
+      server.stop();
+    }
+  }
+  
+  @Test
   public void testHTTPSServer() throws Exception {
     HTTPTools.disableCertChecks();
-    final URL keystore = HTTPSServerTest.class.getResource(KEYSTORE);
-    if (keystore == null) {
-      throw new RuntimeException("Not found in classpath : " + KEYSTORE);
-    }
-    final File keyStoreFile = new File(keystore.getFile());
-    final HTTPSServerConfig config = new HTTPSServerConfig(keyStoreFile, 
KEYSTORE_PASSWORD);
+    final HTTPSServerConfig config = new HTTPSServerConfig(getKeystoreFile(), 
KEYSTORE_PASSWORD);
     config.setMaxTextLength(500);
     final HTTPSServer server = new HTTPSServer(config, false, 
HTTPServerConfig.DEFAULT_HOST, null);
     try {
@@ -54,6 +70,14 @@
     }
   }
 
+  private File getKeystoreFile() {
+    final URL keystore = HTTPSServerTest.class.getResource(KEYSTORE);
+    if (keystore == null) {
+      throw new RuntimeException("Not found in classpath : " + KEYSTORE);
+    }
+    return new File(keystore.getFile());
+  }
+
   private void runTests() throws IOException {
     try {
       final String httpPrefix = "http://localhost:"; + 
HTTPServerConfig.DEFAULT_PORT + "/";
@@ -82,6 +106,13 @@
     } catch (IOException expected) {}
   }
 
+  private String check(Language lang, String text) throws IOException {
+    String urlOptions = "/?language=" + lang.getShortName();
+    urlOptions += "&disabled=HUNSPELL_RULE&text=" + URLEncoder.encode(text, 
"UTF-8"); // latin1 is not enough for languages like polish, romanian, etc
+    final URL url = new URL("https://localhost:"; + DEFAULT_PORT + urlOptions);
+    return HTTPTools.checkAtUrl(url);
+  }
+  
   private String encode(String text) throws UnsupportedEncodingException {
     return URLEncoder.encode(text, "utf-8");
   }

Modified: 
trunk/JLanguageTool/src/test/java/org/languagetool/server/HTTPServerTest.java
===================================================================
--- 
trunk/JLanguageTool/src/test/java/org/languagetool/server/HTTPServerTest.java   
    2013-01-04 08:20:43 UTC (rev 8836)
+++ 
trunk/JLanguageTool/src/test/java/org/languagetool/server/HTTPServerTest.java   
    2013-01-04 12:08:52 UTC (rev 8837)
@@ -163,8 +163,7 @@
         System.out.println("Testing 'access denied' check now, please ignore 
the exception");
         check(Language.GERMAN, "no ip address allowed, so this cannot work");
         fail();
-      } catch (IOException expected) {
-      }
+      } catch (IOException expected) {}
     } finally {
       server.stop();
     }
@@ -180,8 +179,7 @@
         final URL url = new URL("http://localhost:"; + DEFAULT_PORT + 
"/?text=foo");
         HTTPTools.checkAtUrl(url);
         fail();
-      } catch (IOException expected) {
-      }
+      } catch (IOException expected) {}
     } finally {
       server.stop();
     }
@@ -206,7 +204,7 @@
     String urlOptions = "/?language=" + lang.getShortName();
     urlOptions += "&disabled=HUNSPELL_RULE&text=" + URLEncoder.encode(text, 
"UTF-8"); // latin1 is not enough for languages like polish, romanian, etc
     if (null != motherTongue) {
-       urlOptions += "&motherTongue=" + motherTongue.getShortName();
+      urlOptions += "&motherTongue=" + motherTongue.getShortName();
     }
     final URL url = new URL("http://localhost:"; + DEFAULT_PORT + urlOptions);
     return HTTPTools.checkAtUrl(url);

Added: 
trunk/JLanguageTool/src/test/java/org/languagetool/server/RequestLimiterTest.java
===================================================================
--- 
trunk/JLanguageTool/src/test/java/org/languagetool/server/RequestLimiterTest.java
                           (rev 0)
+++ 
trunk/JLanguageTool/src/test/java/org/languagetool/server/RequestLimiterTest.java
   2013-01-04 12:08:52 UTC (rev 8837)
@@ -0,0 +1,46 @@
+/* LanguageTool, a natural language style checker
+ * Copyright (C) 2012 Daniel Naber (http://www.danielnaber.de)
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
+ * USA
+ */
+package org.languagetool.server;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class RequestLimiterTest {
+  
+  @Test
+  public void testIsAccessOkay() throws Exception {
+    final RequestLimiter limiter = new RequestLimiter(3, 2);
+    final String firstIp = "192.168.10.1";
+    final String secondIp = "192.168.10.2";
+    assertTrue(limiter.isAccessOkay(firstIp));
+    assertTrue(limiter.isAccessOkay(firstIp));
+    assertTrue(limiter.isAccessOkay(firstIp));
+    assertFalse(limiter.isAccessOkay(firstIp));
+    assertTrue(limiter.isAccessOkay(secondIp));
+    Thread.sleep(2500);
+    assertTrue(limiter.isAccessOkay(firstIp));
+    assertTrue(limiter.isAccessOkay(secondIp));
+    assertTrue(limiter.isAccessOkay(secondIp));
+    assertTrue(limiter.isAccessOkay(secondIp));
+    assertFalse(limiter.isAccessOkay(secondIp));
+  }
+  
+}

This was sent by the SourceForge.net collaborative development platform, the 
world's largest Open Source development site.


------------------------------------------------------------------------------
Master HTML5, CSS3, ASP.NET, MVC, AJAX, Knockout.js, Web API and
much more. Get web development skills now with LearnDevNow -
350+ hours of step-by-step video tutorials by Microsoft MVPs and experts.
SALE $99.99 this month only -- learn more at:
http://p.sf.net/sfu/learnmore_122812
_______________________________________________
Languagetool-commits mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/languagetool-commits

Reply via email to