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