diff options
author | Jarno Elonen <elonen@iki.fi> | 2015-09-13 20:49:59 +0300 |
---|---|---|
committer | Jarno Elonen <elonen@iki.fi> | 2015-09-13 20:49:59 +0300 |
commit | 60b7e9249246749a16bf8bc29aa117e4201eae8b (patch) | |
tree | 35c559493a5ec0e92846c51e048f683fdd5c4057 | |
parent | 9f38ca21e99986290bf65a8fec17d2592d98d744 (diff) | |
parent | f3b51b6d9e8b24f2f2e021465b0f5972ad12892d (diff) | |
download | nanohttpd-60b7e9249246749a16bf8bc29aa117e4201eae8b.tar.gz |
Merge branch 'master' into static-new-xx
36 files changed, 2088 insertions, 77 deletions
@@ -121,6 +121,19 @@ NanoHTTPD project currently consist of four parts: * File server serves also very long files without memory overhead. * Contains a built-in list of most common MIME types. * Runtime extension support (extensions that serve particular MIME types) - example extension that serves Markdown formatted files. Simply including an extension JAR in the webserver classpath is enough for the extension to be loaded. +* Simple [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) support via `--cors` paramater + * by default serves `Access-Control-Allow-Headers: origin,accept,content-type` + * possibility to set `Access-Control-Allow-Headers` by setting System property: `AccessControlAllowHeader` + * _example: _ `-DAccessControlAllowHeader=origin,accept,content-type,Authorization` + * possible values: + * `--cors`: activates CORS support, `Access-Control-Allow-Origin` will be set to `*` + * `--cors=some_value`: `Access-Control-Allow-Origin` will be set to `some_value`. + +**_CORS argument examples_** + + +* `--cors=http://appOne.company.com` +* `--cors="http://appOne.company.com, http://appTwo.company.com"`: note the double quotes so that the 2 URLs are considered part of a single argument. ## Maven dependencies @@ -198,7 +211,19 @@ The latest Github master version can be fetched through sonatype.org: </repository> </repositories> +### generating an self signed ssl certificate + +Just a hint how to generate a certificate for localhost. + keytool -genkey -keyalg RSA -alias selfsigned -keystore keystore.jks -storepass password -validity 360 -keysize 2048 -ext SAN=DNS:localhost,IP:127.0.0.1 -validity 9999 + +This will generate a keystore file named 'keystore.jks' with a self signed certificate for a host named localhost with the ip adress 127.0.0.1 . Now +you can use: + + server.makeSecure(NanoHTTPD.makeSSLSocketFactory("/keystore.jks", "password".toCharArray())); + +Before you start the server to make Nanohttpd serve https connections, when you make sure 'keystore.jks' is in your classpath . + ----- *Thank you to everyone who has reported bugs and suggested fixes.* diff --git a/core/pom.xml b/core/pom.xml index 1ddc30d..2b65dca 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -22,4 +22,7 @@ <scope>test</scope> </dependency> </dependencies> + <properties> + <minimal.coverage>0.81</minimal.coverage> + </properties> </project> diff --git a/core/src/main/java/fi/iki/elonen/NanoHTTPD.java b/core/src/main/java/fi/iki/elonen/NanoHTTPD.java index 689ef7c..da87bbb 100644 --- a/core/src/main/java/fi/iki/elonen/NanoHTTPD.java +++ b/core/src/main/java/fi/iki/elonen/NanoHTTPD.java @@ -33,7 +33,25 @@ package fi.iki.elonen; * #L% */ -import java.io.*; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.PushbackInputStream; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; @@ -457,6 +475,10 @@ public abstract class NanoHTTPD { protected class HTTPSession implements IHTTPSession { + private static final int REQUEST_BUFFER_LEN = 512; + + private static final int MEMORY_STORE_LIMIT = 1024; + public static final int BUFSIZE = 8192; private final TempFileManager tempFileManager; @@ -902,8 +924,6 @@ public abstract class NanoHTTPD { @Override public void parseBody(Map<String, String> files) throws IOException, ResponseException { - final int REQUEST_BUFFER_LEN = 512; - final int MEMORY_STORE_LIMIT = 1024; RandomAccessFile randomAccessFile = null; try { long size = getBodySize(); @@ -1576,17 +1596,11 @@ public abstract class NanoHTTPD { * by the caller. */ public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException { - SSLServerSocketFactory res = null; try { - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(loadedKeyStore); - SSLContext ctx = SSLContext.getInstance("TLS"); - ctx.init(loadedKeyFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); - res = ctx.getServerSocketFactory(); + return makeSSLSocketFactory(loadedKeyStore, loadedKeyFactory.getKeyManagers()); } catch (Exception e) { throw new IOException(e.getMessage()); } - return res; } /** @@ -1594,22 +1608,16 @@ public abstract class NanoHTTPD { * certificate and passphrase */ public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException { - SSLServerSocketFactory res = null; try { KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath); keystore.load(keystoreStream, passphrase); - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(keystore); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(keystore, passphrase); - SSLContext ctx = SSLContext.getInstance("TLS"); - ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); - res = ctx.getServerSocketFactory(); + return makeSSLSocketFactory(keystore, keyManagerFactory); } catch (Exception e) { throw new IOException(e.getMessage()); } - return res; } private static final void safeClose(Object closeable) { @@ -1634,7 +1642,7 @@ public abstract class NanoHTTPD { private final int myPort; - private ServerSocket myServerSocket; + private volatile ServerSocket myServerSocket; private SSLServerSocketFactory sslServerSocketFactory; diff --git a/webserver/src/main/java/fi/iki/elonen/ServerRunner.java b/core/src/main/java/fi/iki/elonen/util/ServerRunner.java index d08eb01..e0aa3db 100644 --- a/webserver/src/main/java/fi/iki/elonen/ServerRunner.java +++ b/core/src/main/java/fi/iki/elonen/util/ServerRunner.java @@ -1,4 +1,4 @@ -package fi.iki.elonen; +package fi.iki.elonen.util; /* * #%L @@ -37,6 +37,8 @@ import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; +import fi.iki.elonen.NanoHTTPD; + public class ServerRunner { /** @@ -46,7 +48,7 @@ public class ServerRunner { public static void executeInstance(NanoHTTPD server) { try { - server.start(); + server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); } catch (IOException ioe) { System.err.println("Couldn't start server:\n" + ioe); System.exit(-1); diff --git a/core/src/test/java/fi/iki/elonen/HttpHeadRequestTest.java b/core/src/test/java/fi/iki/elonen/HttpHeadRequestTest.java index 4eb3147..9b5983e 100644 --- a/core/src/test/java/fi/iki/elonen/HttpHeadRequestTest.java +++ b/core/src/test/java/fi/iki/elonen/HttpHeadRequestTest.java @@ -45,7 +45,7 @@ import org.junit.Test; public class HttpHeadRequestTest extends HttpServerTest { @Override - public void setUp() { + public void setUp() throws Exception { super.setUp(); String responseBody = "Success!"; this.testServer.response = NanoHTTPD.newFixedLengthResponse(responseBody); diff --git a/core/src/test/java/fi/iki/elonen/HttpSSLServerTest.java b/core/src/test/java/fi/iki/elonen/HttpSSLServerTest.java new file mode 100644 index 0000000..4923cca --- /dev/null +++ b/core/src/test/java/fi/iki/elonen/HttpSSLServerTest.java @@ -0,0 +1,87 @@ +package fi.iki.elonen; + +/* + * #%L + * NanoHttpd-Core + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * 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, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd 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 HOLDER 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. + * #L% + */ + +import java.io.File; +import java.io.IOException; + +import javax.net.ssl.SSLServerSocketFactory; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpTrace; +import org.apache.http.impl.client.DefaultHttpClient; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class HttpSSLServerTest extends HttpServerTest { + + @Test + public void testSSLConnection() throws ClientProtocolException, IOException { + DefaultHttpClient httpclient = new DefaultHttpClient(); + HttpTrace httphead = new HttpTrace("https://localhost:9043/index.html"); + HttpResponse response = httpclient.execute(httphead); + HttpEntity entity = response.getEntity(); + Assert.assertEquals(200, response.getStatusLine().getStatusCode()); + } + + @Before + public void setUp() throws Exception { + System.setProperty("javax.net.ssl.trustStore", new File("src/test/resources/keystore.jks").getAbsolutePath()); + this.testServer = new TestServer(9043); + this.testServer.makeSecure(NanoHTTPD.makeSSLSocketFactory("/keystore.jks", "password".toCharArray())); + this.tempFileManager = new TestTempFileManager(); + this.testServer.start(); + try { + long start = System.currentTimeMillis(); + Thread.sleep(100L); + while (!this.testServer.wasStarted()) { + Thread.sleep(100L); + if (System.currentTimeMillis() - start > 2000) { + Assert.fail("could not start server"); + } + } + } catch (InterruptedException e) { + } + } + + @After + public void tearDown() { + this.testServer.stop(); + } +} diff --git a/core/src/test/java/fi/iki/elonen/HttpServerTest.java b/core/src/test/java/fi/iki/elonen/HttpServerTest.java index 429f5ab..e8be61e 100644 --- a/core/src/test/java/fi/iki/elonen/HttpServerTest.java +++ b/core/src/test/java/fi/iki/elonen/HttpServerTest.java @@ -83,6 +83,10 @@ public class HttpServerTest { super(8192); } + public TestServer(int port) { + super(port); + } + public HTTPSession createSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { return new HTTPSession(tempFileManager, inputStream, outputStream); } @@ -126,7 +130,7 @@ public class HttpServerTest { protected TestServer testServer; - private TestTempFileManager tempFileManager; + protected TestTempFileManager tempFileManager; protected void assertLinesOfText(String[] expected, List<String> lines) { // assertEquals(expected.length, lines.size()); @@ -172,7 +176,7 @@ public class HttpServerTest { } @Before - public void setUp() { + public void setUp() throws Exception { this.testServer = new TestServer(); this.tempFileManager = new TestTempFileManager(); } diff --git a/core/src/test/java/fi/iki/elonen/HttpSessionHeadersTest.java b/core/src/test/java/fi/iki/elonen/HttpSessionHeadersTest.java index 65947b9..1494e28 100644 --- a/core/src/test/java/fi/iki/elonen/HttpSessionHeadersTest.java +++ b/core/src/test/java/fi/iki/elonen/HttpSessionHeadersTest.java @@ -48,11 +48,6 @@ public class HttpSessionHeadersTest extends HttpServerTest { private static final TestTempFileManager TEST_TEMP_FILE_MANAGER = new TestTempFileManager(); - @Override - public void setUp() { - super.setUp(); - } - @Test @Ignore public void testHeadersRemoteIp() throws Exception { diff --git a/core/src/test/resources/keystore.jks b/core/src/test/resources/keystore.jks Binary files differnew file mode 100644 index 0000000..354b5d3 --- /dev/null +++ b/core/src/test/resources/keystore.jks diff --git a/fileupload/.gitignore b/fileupload/.gitignore new file mode 100644 index 0000000..868a6b2 --- /dev/null +++ b/fileupload/.gitignore @@ -0,0 +1,2 @@ +/.settings/ +/LICENSE.txt diff --git a/fileupload/pom.xml b/fileupload/pom.xml new file mode 100644 index 0000000..0be88a9 --- /dev/null +++ b/fileupload/pom.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <artifactId>nanohttpd-project</artifactId> + <groupId>org.nanohttpd</groupId> + <version>2.2.0-SNAPSHOT</version> + </parent> + <modelVersion>4.0.0</modelVersion> + <artifactId>nanohttpd-apache-fileupload</artifactId> + <name>NanoHttpd-apache file upload integration</name> + <dependencies> + <dependency> + <groupId>org.nanohttpd</groupId> + <artifactId>nanohttpd</artifactId> + <version>2.2.0-SNAPSHOT</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>commons-fileupload</groupId> + <artifactId>commons-fileupload</artifactId> + <version>1.3.1</version> + </dependency> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>servlet-api</artifactId> + <version>2.5</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + <version>4.4.1</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpmime</artifactId> + <version>4.4.1</version> + <scope>test</scope> + </dependency> + </dependencies> + <properties> + <minimal.coverage>0.99</minimal.coverage> + </properties> +</project>
\ No newline at end of file diff --git a/fileupload/src/main/java/fi/iki/elonen/NanoFileUpload.java b/fileupload/src/main/java/fi/iki/elonen/NanoFileUpload.java new file mode 100644 index 0000000..ec02d4a --- /dev/null +++ b/fileupload/src/main/java/fi/iki/elonen/NanoFileUpload.java @@ -0,0 +1,118 @@ +package fi.iki.elonen; + +/* + * #%L + * apache-fileupload-integration + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * 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, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd 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 HOLDER 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. + * #L% + */ + +import static fi.iki.elonen.NanoHTTPD.Method.POST; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileItemFactory; +import org.apache.commons.fileupload.FileItemIterator; +import org.apache.commons.fileupload.FileUpload; +import org.apache.commons.fileupload.FileUploadBase; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload.UploadContext; + +/** + * @author victor & ritchieGitHub + */ +public class NanoFileUpload extends FileUpload { + + public static class NanoHttpdContext implements UploadContext { + + private NanoHTTPD.IHTTPSession session; + + public NanoHttpdContext(NanoHTTPD.IHTTPSession session) { + this.session = session; + } + + @Override + public long contentLength() { + long size; + try { + String cl1 = session.getHeaders().get("content-length"); + size = Long.parseLong(cl1); + } catch (NumberFormatException var4) { + size = -1L; + } + + return size; + } + + @Override + public String getCharacterEncoding() { + return "UTF-8"; + } + + @Override + public String getContentType() { + return this.session.getHeaders().get("content-type"); + } + + @Override + public int getContentLength() { + return (int) contentLength(); + } + + @Override + public InputStream getInputStream() throws IOException { + return session.getInputStream(); + } + } + + public static final boolean isMultipartContent(NanoHTTPD.IHTTPSession session) { + return session.getMethod() == POST && FileUploadBase.isMultipartContent(new NanoHttpdContext(session)); + } + + public NanoFileUpload(FileItemFactory fileItemFactory) { + super(fileItemFactory); + } + + public List<FileItem> parseRequest(NanoHTTPD.IHTTPSession session) throws FileUploadException { + return this.parseRequest(new NanoHttpdContext(session)); + } + + public Map<String, List<FileItem>> parseParameterMap(NanoHTTPD.IHTTPSession session) throws FileUploadException { + return this.parseParameterMap(new NanoHttpdContext(session)); + } + + public FileItemIterator getItemIterator(NanoHTTPD.IHTTPSession session) throws FileUploadException, IOException { + return super.getItemIterator(new NanoHttpdContext(session)); + } + +} diff --git a/fileupload/src/test/java/fi/iki/elonen/TestNanoFileUpLoad.java b/fileupload/src/test/java/fi/iki/elonen/TestNanoFileUpLoad.java new file mode 100644 index 0000000..f04375d --- /dev/null +++ b/fileupload/src/test/java/fi/iki/elonen/TestNanoFileUpLoad.java @@ -0,0 +1,251 @@ +package fi.iki.elonen; + +/* + * #%L + * NanoHttpd-apache file upload integration + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * 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, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd 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 HOLDER 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. + * #L% + */ + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileItemIterator; +import org.apache.commons.fileupload.FileItemStream; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.fileupload.util.Streams; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpTrace; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.entity.mime.content.FileBody; +import org.apache.http.entity.mime.content.StringBody; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.internal.runners.statements.Fail; + +import fi.iki.elonen.NanoHTTPD.Response.Status; + +/** + * very strange but if the file upload is the first request the test fails. + * + * @author ritchieGitHub + */ +@FixMethodOrder +public class TestNanoFileUpLoad { + + protected TestServer testServer; + + public static class TestServer extends NanoHTTPD { + + public Response response = newFixedLengthResponse(""); + + public String uri; + + public Method method; + + public Map<String, String> header; + + public Map<String, String> parms; + + public Map<String, List<FileItem>> files; + + public Map<String, List<String>> decodedParamters; + + public Map<String, List<String>> decodedParamtersFromParameter; + + public String queryParameterString; + + public TestServer() { + super(8192); + uploader = new NanoFileUpload(new DiskFileItemFactory()); + } + + public HTTPSession createSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) { + return new HTTPSession(tempFileManager, inputStream, outputStream); + } + + public HTTPSession createSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) { + return new HTTPSession(tempFileManager, inputStream, outputStream, inetAddress); + } + + @Override + public String decodePercent(String str) { + return super.decodePercent(str); + } + + NanoFileUpload uploader; + + @Override + public Response serve(IHTTPSession session) { + + this.uri = session.getUri(); + this.method = session.getMethod(); + this.header = session.getHeaders(); + this.parms = session.getParms(); + if (NanoFileUpload.isMultipartContent(session)) { + try { + if ("/uploadFile1".equals(this.uri)) { + session.getHeaders().put("content-length", "AA"); + files = uploader.parseParameterMap(session); + } + if ("/uploadFile2".equals(this.uri)) { + files = new HashMap<String, List<FileItem>>(); + List<FileItem> parseRequest = uploader.parseRequest(session); + files.put(parseRequest.get(0).getFieldName(), parseRequest); + } + if ("/uploadFile3".equals(this.uri)) { + files = new HashMap<String, List<FileItem>>(); + FileItemIterator iter = uploader.getItemIterator(session); + while (iter.hasNext()) { + FileItemStream item = iter.next(); + final String fileName = item.getName(); + FileItem fileItem = uploader.getFileItemFactory().createItem(item.getFieldName(), item.getContentType(), item.isFormField(), fileName); + files.put(fileItem.getFieldName(), Arrays.asList(new FileItem[]{ + fileItem + })); + try { + Streams.copy(item.openStream(), fileItem.getOutputStream(), true); + } catch (Exception e) { + } + fileItem.setHeaders(item.getHeaders()); + } + } + } catch (Exception e) { + this.response.setStatus(Status.INTERNAL_ERROR); + e.printStackTrace(); + } + } + this.queryParameterString = session.getQueryParameterString(); + this.decodedParamtersFromParameter = decodeParameters(this.queryParameterString); + this.decodedParamters = decodeParameters(session.getQueryParameterString()); + return this.response; + } + + } + + @Test + public void testNormalRequest() throws Exception { + CloseableHttpClient httpclient = HttpClients.createDefault(); + HttpTrace httphead = new HttpTrace("http://localhost:8192/index.html"); + CloseableHttpResponse response = httpclient.execute(httphead); + Assert.assertEquals(200, response.getStatusLine().getStatusCode()); + response.close(); + } + + @Test + public void testPostWithMultipartFormUpload1() throws Exception { + CloseableHttpClient httpclient = HttpClients.createDefault(); + String textFileName = "src/test/java/fi/iki/elonen/TestNanoFileUpLoad.java"; + HttpPost post = new HttpPost("http://localhost:8192/uploadFile1"); + + executeUpload(httpclient, textFileName, post); + FileItem file = this.testServer.files.get("upfile").get(0); + Assert.assertEquals(file.getSize(), new File(textFileName).length()); + } + + @Test + public void testPostWithMultipartFormUpload2() throws Exception { + CloseableHttpClient httpclient = HttpClients.createDefault(); + String textFileName = "src/test/java/fi/iki/elonen/TestNanoFileUpLoad.java"; + HttpPost post = new HttpPost("http://localhost:8192/uploadFile2"); + + executeUpload(httpclient, textFileName, post); + FileItem file = this.testServer.files.get("upfile").get(0); + Assert.assertEquals(file.getSize(), new File(textFileName).length()); + } + + @Test + public void testPostWithMultipartFormUpload3() throws Exception { + CloseableHttpClient httpclient = HttpClients.createDefault(); + String textFileName = "src/test/java/fi/iki/elonen/TestNanoFileUpLoad.java"; + HttpPost post = new HttpPost("http://localhost:8192/uploadFile3"); + + executeUpload(httpclient, textFileName, post); + FileItem file = this.testServer.files.get("upfile").get(0); + Assert.assertEquals(file.getSize(), new File(textFileName).length()); + } + + private void executeUpload(CloseableHttpClient httpclient, String textFileName, HttpPost post) throws IOException, ClientProtocolException { + FileBody fileBody = new FileBody(new File(textFileName), ContentType.DEFAULT_BINARY); + StringBody stringBody1 = new StringBody("Message 1", ContentType.MULTIPART_FORM_DATA); + + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); + builder.addPart("upfile", fileBody); + builder.addPart("text1", stringBody1); + HttpEntity entity = builder.build(); + // + post.setEntity(entity); + HttpResponse response = httpclient.execute(post); + Assert.assertEquals(200, response.getStatusLine().getStatusCode()); + } + + @Before + public void setUp() throws IOException { + this.testServer = new TestServer(); + this.testServer.start(); + try { + long start = System.currentTimeMillis(); + Thread.sleep(100L); + while (!this.testServer.wasStarted()) { + Thread.sleep(100L); + if (System.currentTimeMillis() - start > 2000) { + Assert.fail("could not start server"); + } + } + } catch (InterruptedException e) { + } + } + + @After + public void tearDown() { + this.testServer.stop(); + } + +} diff --git a/markdown-plugin/pom.xml b/markdown-plugin/pom.xml index 590bb44..f7034c7 100644 --- a/markdown-plugin/pom.xml +++ b/markdown-plugin/pom.xml @@ -56,4 +56,7 @@ </plugin> </plugins> </build> + <properties> + <minimal.coverage>0.0</minimal.coverage> + </properties> </project> diff --git a/nanolets/.settings/org.eclipse.core.resources.prefs b/nanolets/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..839d647 --- /dev/null +++ b/nanolets/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,5 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding//src/test/java=UTF-8 +encoding/<project>=UTF-8 diff --git a/nanolets/.settings/org.eclipse.jdt.core.prefs b/nanolets/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..60105c1 --- /dev/null +++ b/nanolets/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,5 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/nanolets/.settings/org.eclipse.m2e.core.prefs b/nanolets/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/nanolets/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/nanolets/LICENSE.txt b/nanolets/LICENSE.txt new file mode 100644 index 0000000..fcebfe1 --- /dev/null +++ b/nanolets/LICENSE.txt @@ -0,0 +1,26 @@ +Copyright (c) 2012 - 2015, nanohttpd + +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, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the nanohttpd 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 HOLDER 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. diff --git a/nanolets/pom.xml b/nanolets/pom.xml new file mode 100644 index 0000000..a190abb --- /dev/null +++ b/nanolets/pom.xml @@ -0,0 +1,28 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.nanohttpd</groupId> + <artifactId>nanohttpd-project</artifactId> + <version>2.2.0-SNAPSHOT</version> + </parent> + <artifactId>nanohttpd-nanolets</artifactId> + <packaging>jar</packaging> + <name>NanoHttpd-nano application server</name> + <dependencies> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>nanohttpd</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + <version>4.4.1</version> + <scope>test</scope> + </dependency> + </dependencies> + <properties> + <minimal.coverage>0.98</minimal.coverage> + </properties> +</project> diff --git a/nanolets/src/main/java/fi/iki/elonen/router/RouterNanoHTTPD.java b/nanolets/src/main/java/fi/iki/elonen/router/RouterNanoHTTPD.java new file mode 100644 index 0000000..82dca8b --- /dev/null +++ b/nanolets/src/main/java/fi/iki/elonen/router/RouterNanoHTTPD.java @@ -0,0 +1,546 @@ +package fi.iki.elonen.router; + +/* + * #%L + * NanoHttpd-Samples + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * 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, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd 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 HOLDER 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. + * #L% + */ + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import fi.iki.elonen.NanoHTTPD; +import fi.iki.elonen.NanoHTTPD.Response.IStatus; +import fi.iki.elonen.NanoHTTPD.Response.Status; + +/** + * @author vnnv + * @author ritchieGitHub + */ +public class RouterNanoHTTPD extends NanoHTTPD { + + /** + * logger to log to. + */ + private static final Logger LOG = Logger.getLogger(RouterNanoHTTPD.class.getName()); + + public interface UriResponder { + + public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session); + + public Response put(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session); + + public Response post(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session); + + public Response delete(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session); + + public Response other(String method, UriResource uriResource, Map<String, String> urlParams, IHTTPSession session); + } + + /** + * General nanolet to inherit from if you provide stream data, only chucked + * responses will be generated. + */ + public static abstract class DefaultStreamHandler implements UriResponder { + + public abstract String getMimeType(); + + public abstract IStatus getStatus(); + + public abstract InputStream getData(); + + public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) { + return NanoHTTPD.newChunkedResponse(getStatus(), getMimeType(), getData()); + } + + public Response post(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) { + return get(uriResource, urlParams, session); + } + + public Response put(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) { + return get(uriResource, urlParams, session); + } + + public Response delete(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) { + return get(uriResource, urlParams, session); + } + + public Response other(String method, UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) { + return get(uriResource, urlParams, session); + } + } + + /** + * General nanolet to inherit from if you provide text or html data, only + * fixed size responses will be generated. + */ + public static abstract class DefaultHandler extends DefaultStreamHandler { + + public abstract String getText(); + + public abstract IStatus getStatus(); + + public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) { + return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), getText()); + } + + @Override + public InputStream getData() { + throw new IllegalStateException("this method should not be called in a text based nanolet"); + } + } + + /** + * General nanolet to print debug info's as a html page. + */ + public static class GeneralHandler extends DefaultHandler { + + @Override + public String getText() { + throw new IllegalStateException("this method should not be called"); + } + + @Override + public String getMimeType() { + return "text/html"; + } + + @Override + public IStatus getStatus() { + return Status.OK; + } + + public Response get(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) { + StringBuilder text = new StringBuilder("<html><body>"); + text.append("<h1>Url: "); + text.append(session.getUri()); + text.append("</h1><br>"); + Map<String, String> queryParams = session.getParms(); + if (queryParams.size() > 0) { + for (Map.Entry<String, String> entry : queryParams.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + text.append("<p>Param '"); + text.append(key); + text.append("' = "); + text.append(value); + text.append("</p>"); + } + } else { + text.append("<p>no params in url</p><br>"); + } + return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), text.toString()); + } + } + + /** + * Handling error 404 - unrecognized urls + */ + public static class Error404UriHandler extends DefaultHandler { + + public String getText() { + return "<html><body><h3>Error 404: the requested page doesn't exist.</h3></body></html>"; + } + + @Override + public String getMimeType() { + return "text/html"; + } + + @Override + public IStatus getStatus() { + return Status.NOT_FOUND; + } + } + + /** + * Handling index + */ + public static class IndexHandler extends DefaultHandler { + + public String getText() { + return "<html><body><h2>Hello world!</h3></body></html>"; + } + + @Override + public String getMimeType() { + return "text/html"; + } + + @Override + public IStatus getStatus() { + return Status.OK; + } + + } + + public static class NotImplementedHandler extends DefaultHandler { + + public String getText() { + return "<html><body><h2>The uri is mapped in the router, but no handler is specified. <br> Status: Not implemented!</h3></body></html>"; + } + + @Override + public String getMimeType() { + return "text/html"; + } + + @Override + public IStatus getStatus() { + return Status.OK; + } + } + + public static String normalizeUri(String value) { + if (value == null) { + return value; + } + if (value.startsWith("/")) { + value = value.substring(1); + } + if (value.endsWith("/")) { + value = value.substring(0, value.length() - 1); + } + return value; + + } + + public static class UriPart { + + private String name; + + private boolean isParam; + + public UriPart(String name, boolean isParam) { + this.name = name; + this.isParam = isParam; + } + + @Override + public String toString() { + return new StringBuilder("UriPart{name='").append(name)// + .append("\', isParam=").append(isParam)// + .append('}').toString(); + } + + public boolean isParam() { + return isParam; + } + + public String getName() { + return name; + } + + } + + public static class UriResource { + + private boolean hasParameters; + + private int uriParamsCount; + + private String uri; + + private List<UriPart> uriParts; + + private Class<?> handler; + + public UriResource(String uri, Class<?> handler) { + this.hasParameters = false; + this.handler = handler; + uriParamsCount = 0; + if (uri != null) { + this.uri = normalizeUri(uri); + parse(); + } + } + + private void parse() { + String[] parts = uri.split("/"); + uriParts = new ArrayList<UriPart>(); + for (String part : parts) { + boolean isParam = part.startsWith(":"); + UriPart uriPart = null; + if (isParam) { + hasParameters = true; + uriParamsCount++; + uriPart = new UriPart(part.substring(1), true); + } else { + uriPart = new UriPart(part, false); + } + uriParts.add(uriPart); + } + + } + + public Response process(Map<String, String> urlParams, IHTTPSession session) { + String error = "General error!"; + if (handler != null) { + try { + Object object = handler.newInstance(); + if (object instanceof UriResponder) { + UriResponder responder = (UriResponder) object; + switch (session.getMethod()) { + case GET: + return responder.get(this, urlParams, session); + case POST: + return responder.post(this, urlParams, session); + case PUT: + return responder.put(this, urlParams, session); + case DELETE: + return responder.delete(this, urlParams, session); + default: + return responder.other(session.getMethod().toString(), this, urlParams, session); + } + } else { + return NanoHTTPD.newFixedLengthResponse(Status.OK, "text/plain", // + new StringBuilder("Return: ")// + .append(handler.getCanonicalName())// + .append(".toString() -> ")// + .append(object)// + .toString()); + } + } catch (Exception e) { + error = "Error: " + e.getClass().getName() + " : " + e.getMessage(); + LOG.log(Level.SEVERE, error, e); + } + } + return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR, "text/plain", error); + } + + @Override + public String toString() { + return new StringBuilder("UrlResource{hasParameters=").append(hasParameters)// + .append(", uriParamsCount=").append(uriParamsCount)// + .append(", uri='").append((uri == null ? "/" : uri))// + .append("', urlParts=").append(uriParts)// + .append('}')// + .toString(); + } + + public boolean hasParameters() { + return hasParameters; + } + + public String getUri() { + return uri; + } + + public List<UriPart> getUriParts() { + return uriParts; + } + + public int getUriParamsCount() { + return uriParamsCount; + } + + } + + public static class UriRouter { + + private List<UriResource> mappings; + + private UriResource error404Url; + + private Class<?> notImplemented; + + public UriRouter() { + mappings = new ArrayList<UriResource>(); + } + + /** + * Search in the mappings if the given url matches some of the rules If + * there are more than one marches returns the rule with less parameters + * e.g. mapping 1 = /user/:id mapping 2 = /user/help if the incoming uri + * is www.example.com/user/help - mapping 2 is returned if the incoming + * uri is www.example.com/user/3232 - mapping 1 is returned + * + * @param url + * @return + */ + public UriResource matchUrl(String url) { + String work = normalizeUri(url); + String[] parts = work.split("/"); + List<UriResource> resultList = new ArrayList<UriResource>(); + for (UriResource u : mappings) { + // Check if count of parts is the same + if (parts.length != u.getUriParts().size()) { + continue; // different + } + List<UriPart> uriParts = u.getUriParts(); + boolean match = true; + for (int i = 0; i < parts.length; i++) { + String currentPart = parts[i]; + UriPart uriPart = uriParts.get(i); + boolean goOn = false; + if (currentPart.equals(uriPart.getName())) { + // exact match + goOn = true; + } else { + // not match + if (uriPart.isParam()) { + goOn = true; + } else { + match = false; + goOn = false; + } + } + if (!goOn) { + match = false; + break; + } + } + if (match) { + // current match + resultList.add(u); + } + } + if (!resultList.isEmpty()) { + // some results + UriResource result = null; + if (resultList.size() > 1) { + // return the rule with less parameters + int params = 1024; + for (UriResource u : resultList) { + if (!u.hasParameters()) { + result = u; + break; + } else { + if (u.getUriParamsCount() <= params) { + result = u; + } + } + } + return result; + } else { + return resultList.get(0); + } + } + return error404Url; + } + + public void addRoute(String url, Class<?> handler) { + if (url != null) { + if (handler != null) { + mappings.add(new UriResource(url, handler)); + } else { + mappings.add(new UriResource(url, notImplemented)); + } + } + } + + public void removeRoute(String url) { + String uriToDelete = normalizeUri(url); + Iterator<UriResource> iter = mappings.iterator(); + while (iter.hasNext()) { + UriResource uriResource = iter.next(); + if (uriToDelete.equals(uriResource.getUri())) { + iter.remove(); + break; + } + } + } + + public void setNotFoundHandler(Class<?> handler) { + error404Url = new UriResource(null, handler); + } + + public void setNotImplemented(Class<?> handler) { + notImplemented = handler; + } + + /** + * Extract parameters and their values for the given route + * + * @param route + * @param uri + * @return + */ + public Map<String, String> getUrlParams(UriResource route, String uri) { + Map<String, String> result = new HashMap<String, String>(); + if (route.getUri() == null) { + return result; + } + String workUri = normalizeUri(uri); + String[] parts = workUri.split("/"); + for (int i = 0; i < parts.length; i++) { + UriPart currentPart = route.getUriParts().get(i); + if (currentPart.isParam()) { + result.put(currentPart.getName(), parts[i]); + } + } + return result; + } + } + + private UriRouter router; + + public RouterNanoHTTPD(int port) { + super(port); + router = new UriRouter(); + } + + /** + * default routings, they are over writable. + * + * <pre> + * router.setNotFoundHandler(GeneralHandler.class); + * </pre> + */ + + public void addMappings() { + router.setNotImplemented(NotImplementedHandler.class); + router.setNotFoundHandler(Error404UriHandler.class); + router.addRoute("/", IndexHandler.class); + router.addRoute("/index.html", IndexHandler.class); + } + + public void addRoute(String url, Class<?> handler) { + router.addRoute(url, handler); + } + + public void removeRoute(String url) { + router.removeRoute(url); + } + + @Override + public Response serve(IHTTPSession session) { + // Try to find match + UriResource uriResource = router.matchUrl(session.getUri()); + // Process the uri + return uriResource.process(router.getUrlParams(uriResource, session.getUri()), session); + } +} diff --git a/nanolets/src/test/java/fi/iki/elonen/router/AppNanolets.java b/nanolets/src/test/java/fi/iki/elonen/router/AppNanolets.java new file mode 100644 index 0000000..b2ae4ed --- /dev/null +++ b/nanolets/src/test/java/fi/iki/elonen/router/AppNanolets.java @@ -0,0 +1,158 @@ +package fi.iki.elonen.router; + +/* + * #%L + * NanoHttpd-Samples + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * 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, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd 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 HOLDER 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. + * #L% + */ + +/** + * Created by vnnv on 7/17/15. + * Simple httpd server based on NanoHTTPD + * Read the source. Everything is there. + */ + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import fi.iki.elonen.NanoHTTPD; +import fi.iki.elonen.NanoHTTPD.Response.IStatus; +import fi.iki.elonen.NanoHTTPD.Response.Status; +import fi.iki.elonen.util.ServerRunner; + +public class AppNanolets extends RouterNanoHTTPD { + + private static final int PORT = 9090; + + public static class UserHandler extends DefaultHandler { + + @Override + public String getText() { + return "not implemented"; + } + + public String getText(Map<String, String> urlParams, NanoHTTPD.IHTTPSession session) { + String text = "<html><body>User handler. Method: " + session.getMethod().toString() + "<br>"; + text += "<h1>Uri parameters:</h1>"; + for (Map.Entry<String, String> entry : urlParams.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + text += "<div> Param: " + key + " Value: " + value + "</div>"; + } + text += "<h1>Query parameters:</h1>"; + for (Map.Entry<String, String> entry : session.getParms().entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + text += "<div> Query Param: " + key + " Value: " + value + "</div>"; + } + text += "</body></html>"; + + return text; + } + + @Override + public String getMimeType() { + return "text/html"; + } + + @Override + public NanoHTTPD.Response.IStatus getStatus() { + return NanoHTTPD.Response.Status.OK; + } + + public NanoHTTPD.Response get(UriResource uriResource, Map<String, String> urlParams, NanoHTTPD.IHTTPSession session) { + String text = getText(urlParams, session); + ByteArrayInputStream inp = new ByteArrayInputStream(text.getBytes()); + int size = text.getBytes().length; + return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), inp, size); + } + + } + + static public class StreamUrl extends DefaultStreamHandler { + + @Override + public String getMimeType() { + return "text/plain"; + } + + @Override + public IStatus getStatus() { + return Status.OK; + } + + @Override + public InputStream getData() { + return new ByteArrayInputStream("a stream of data ;-)".getBytes()); + } + + } + + /** + * Create the server instance + */ + public AppNanolets() throws IOException { + super(PORT); + addMappings(); + System.out.println("\nRunning! Point your browers to http://localhost:" + PORT + "/ \n"); + } + + /** + * Add the routes Every route is an absolute path Parameters starts with ":" + * Handler class should implement @UriResponder interface If the handler not + * implement UriResponder interface - toString() is used + */ + @Override + public void addMappings() { + super.addMappings(); + addRoute("/user", UserHandler.class); + addRoute("/user/:id", UserHandler.class); + addRoute("/user/help", GeneralHandler.class); + addRoute("/general/:param1/:param2", GeneralHandler.class); + addRoute("/photos/:customer_id/:photo_id", null); + addRoute("/test", String.class); + addRoute("/interface", UriResponder.class); // this will cause an error + // when called + addRoute("/toBeDeleted", String.class); + removeRoute("/toBeDeleted"); + addRoute("/stream", StreamUrl.class); + } + + /** + * Main entry point + * + * @param args + */ + public static void main(String[] args) { + ServerRunner.run(AppNanolets.class); + } +} diff --git a/nanolets/src/test/java/fi/iki/elonen/router/TestNanolets.java b/nanolets/src/test/java/fi/iki/elonen/router/TestNanolets.java new file mode 100644 index 0000000..bd360da --- /dev/null +++ b/nanolets/src/test/java/fi/iki/elonen/router/TestNanolets.java @@ -0,0 +1,283 @@ +package fi.iki.elonen.router; + +/* + * #%L + * NanoHttpd nano application server + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * 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, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd 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 HOLDER 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. + * #L% + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; + +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpTrace; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import fi.iki.elonen.router.RouterNanoHTTPD.GeneralHandler; +import fi.iki.elonen.router.RouterNanoHTTPD.UriResource; + +public class TestNanolets { + + private static PipedOutputStream stdIn; + + private static Thread serverStartThread; + + @BeforeClass + public static void setUp() throws Exception { + stdIn = new PipedOutputStream(); + System.setIn(new PipedInputStream(stdIn)); + serverStartThread = new Thread(new Runnable() { + + @Override + public void run() { + String[] args = {}; + AppNanolets.main(args); + } + }); + serverStartThread.start(); + // give the server some tine to start. + Thread.sleep(100); + } + + @Test + public void doSomeBasicMethodTest() throws Exception { + CloseableHttpClient httpclient = HttpClients.createDefault(); + + HttpGet httpget = new HttpGet("http://localhost:9090/user/blabla"); + CloseableHttpResponse response = httpclient.execute(httpget); + HttpEntity entity = response.getEntity(); + String string = new String(readContents(entity), "UTF-8"); + Assert.assertEquals( + "<html><body>User handler. Method: GET<br><h1>Uri parameters:</h1><div> Param: id Value: blabla</div><h1>Query parameters:</h1></body></html>", string); + response.close(); + + HttpPost httppost = new HttpPost("http://localhost:9090/user/blabla"); + response = httpclient.execute(httppost); + entity = response.getEntity(); + string = new String(readContents(entity), "UTF-8"); + Assert.assertEquals( + "<html><body>User handler. Method: POST<br><h1>Uri parameters:</h1><div> Param: id Value: blabla</div><h1>Query parameters:</h1></body></html>", string); + response.close(); + + HttpPut httpgput = new HttpPut("http://localhost:9090/user/blabla"); + response = httpclient.execute(httpgput); + entity = response.getEntity(); + string = new String(readContents(entity), "UTF-8"); + Assert.assertEquals( + "<html><body>User handler. Method: PUT<br><h1>Uri parameters:</h1><div> Param: id Value: blabla</div><h1>Query parameters:</h1></body></html>", string); + response.close(); + + HttpDelete httpdelete = new HttpDelete("http://localhost:9090/user/blabla"); + response = httpclient.execute(httpdelete); + entity = response.getEntity(); + string = new String(readContents(entity), "UTF-8"); + Assert.assertEquals( + "<html><body>User handler. Method: DELETE<br><h1>Uri parameters:</h1><div> Param: id Value: blabla</div><h1>Query parameters:</h1></body></html>", string); + response.close(); + } + + @Test + public void doNonRouterRequest() throws Exception { + CloseableHttpClient httpclient = HttpClients.createDefault(); + + HttpGet httpget = new HttpGet("http://localhost:9090/test"); + CloseableHttpResponse response = httpclient.execute(httpget); + HttpEntity entity = response.getEntity(); + String string = new String(readContents(entity), "UTF-8"); + Assert.assertEquals("Return: java.lang.String.toString() -> ", string); + response.close(); + } + + @Test + public void doExceptionRequest() throws Exception { + CloseableHttpClient httpclient = HttpClients.createDefault(); + + HttpGet httpget = new HttpGet("http://localhost:9090/interface"); + CloseableHttpResponse response = httpclient.execute(httpget); + HttpEntity entity = response.getEntity(); + String string = new String(readContents(entity), "UTF-8"); + Assert.assertEquals("Error: java.lang.InstantiationException : fi.iki.elonen.router.RouterNanoHTTPD$UriResponder", string); + response.close(); + } + + @Test + public void doDeletedRoute() throws Exception { + CloseableHttpClient httpclient = HttpClients.createDefault(); + + HttpGet httpget = new HttpGet("http://localhost:9090/toBeDeleted"); + CloseableHttpResponse response = httpclient.execute(httpget); + HttpEntity entity = response.getEntity(); + String string = new String(readContents(entity), "UTF-8"); + Assert.assertEquals("<html><body><h3>Error 404: the requested page doesn't exist.</h3></body></html>", string); + response.close(); + } + + @Test + public void doUriSelection1() throws Exception { + CloseableHttpClient httpclient = HttpClients.createDefault(); + + HttpGet httpget = new HttpGet("http://localhost:9090/user/help"); + CloseableHttpResponse response = httpclient.execute(httpget); + HttpEntity entity = response.getEntity(); + String string = new String(readContents(entity), "UTF-8"); + Assert.assertEquals("<html><body><h1>Url: /user/help</h1><br><p>no params in url</p><br>", string); + response.close(); + } + + @Test + public void doStreamOfData() throws Exception { + CloseableHttpClient httpclient = HttpClients.createDefault(); + + HttpGet httpget = new HttpGet("http://localhost:9090/stream"); + CloseableHttpResponse response = httpclient.execute(httpget); + HttpEntity entity = response.getEntity(); + String string = new String(readContents(entity), "UTF-8"); + Assert.assertEquals("a stream of data ;-)", string); + response.close(); + } + + @Test(expected = IllegalStateException.class) + public void illegalMethod1() throws Exception { + new AppNanolets.UserHandler().getData(); + } + + @Test(expected = IllegalStateException.class) + public void illegalMethod2() throws Exception { + new RouterNanoHTTPD.GeneralHandler().getText(); + } + + @Test + public void doGeneralParams() throws Exception { + CloseableHttpClient httpclient = HttpClients.createDefault(); + + HttpGet httpget = new HttpGet("http://localhost:9090/general/value1/value2?param3=value3¶m4=value4"); + + CloseableHttpResponse response = httpclient.execute(httpget); + HttpEntity entity = response.getEntity(); + String string = new String(readContents(entity), "UTF-8"); + Assert.assertEquals("<html><body><h1>Url: /general/value1/value2</h1><br><p>Param 'param3' = value3</p><p>Param 'param4' = value4</p>", string); + response.close(); + } + + @Test + public void doIndexHandler() throws Exception { + CloseableHttpClient httpclient = HttpClients.createDefault(); + + HttpGet httpget = new HttpGet("http://localhost:9090/index.html"); + CloseableHttpResponse response = httpclient.execute(httpget); + HttpEntity entity = response.getEntity(); + String string = new String(readContents(entity), "UTF-8"); + Assert.assertEquals("<html><body><h2>Hello world!</h3></body></html>", string); + response.close(); + } + + @Test + public void doMissingHandler() throws Exception { + CloseableHttpClient httpclient = HttpClients.createDefault(); + + HttpGet httpget = new HttpGet("http://localhost:9090/photos/abc/def"); + CloseableHttpResponse response = httpclient.execute(httpget); + HttpEntity entity = response.getEntity(); + String string = new String(readContents(entity), "UTF-8"); + Assert.assertEquals("<html><body><h2>The uri is mapped in the router, but no handler is specified. <br> Status: Not implemented!</h3></body></html>", string); + response.close(); + } + + @Test + public void uriToString() throws Exception { + Assert.assertEquals(// + "UrlResource{hasParameters=true, uriParamsCount=2, uri='photos/:customer_id/:photo_id', urlParts=[UriPart{name='photos', isParam=false}, UriPart{name='customer_id', isParam=true}, UriPart{name='photo_id', isParam=true}]}",// + new UriResource("/photos/:customer_id/:photo_id", GeneralHandler.class).toString()); + } + + @Test + public void doOtherMethod() throws Exception { + CloseableHttpClient httpclient = HttpClients.createDefault(); + + HttpTrace httphead = new HttpTrace("http://localhost:9090/index.html"); + CloseableHttpResponse response = httpclient.execute(httphead); + HttpEntity entity = response.getEntity(); + String string = new String(readContents(entity), "UTF-8"); + Assert.assertEquals("<html><body><h2>Hello world!</h3></body></html>", string); + response.close(); + } + + @Test + public void normalize() throws Exception { + Assert.assertNull(RouterNanoHTTPD.normalizeUri(null)); + Assert.assertEquals("", RouterNanoHTTPD.normalizeUri("/")); + Assert.assertEquals("xxx/yyy", RouterNanoHTTPD.normalizeUri("/xxx/yyy")); + Assert.assertEquals("xxx/yyy", RouterNanoHTTPD.normalizeUri("/xxx/yyy/")); + } + + private byte[] readContents(HttpEntity entity) throws IOException { + InputStream instream = entity.getContent(); + return readContents(instream); + } + + private byte[] readContents(InputStream instream) throws IOException { + byte[] bytes; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + try { + byte[] buffer = new byte[1024]; + int count; + while ((count = instream.read(buffer)) >= 0) { + out.write(buffer, 0, count); + } + bytes = out.toByteArray(); + } finally { + instream.close(); + } + return bytes; + } + + @AfterClass + public static void tearDown() throws Exception { + stdIn.write("\n\n".getBytes()); + serverStartThread.join(2000); + Assert.assertFalse(serverStartThread.isAlive()); + } + +} @@ -8,7 +8,7 @@ </parent> <groupId>org.nanohttpd</groupId> <artifactId>nanohttpd-project</artifactId> - <version>2.2.1-SNAPSHOT</version> + <version>2.2.0-SNAPSHOT</version> <packaging>pom</packaging> <name>NanoHttpd-Project</name> <description>NanoHttpd is a light-weight HTTP server designed for embedding in other applications.</description> @@ -88,6 +88,8 @@ <module>webserver</module> <module>websocket</module> <module>markdown-plugin</module> + <module>nanolets</module> + <module>fileupload</module> </modules> <licenses> <license> @@ -231,6 +233,26 @@ <goal>report</goal> </goals> </execution> + <execution> + <id>default-check</id> + <goals> + <goal>check</goal> + </goals> + <configuration> + <rules> + <rule> + <element>BUNDLE</element> + <limits> + <limit> + <counter>LINE</counter> + <value>COVEREDRATIO</value> + <minimum>${minimal.coverage}</minimum> + </limit> + </limits> + </rule> + </rules> + </configuration> + </execution> </executions> </plugin> </plugins> @@ -318,7 +340,7 @@ <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> - <version>4.8.2</version> + <version>4.12</version> <scope>test</scope> </dependency> </dependencies> @@ -473,4 +495,7 @@ </build> </profile> </profiles> + <properties> + <minimal.coverage>0.77</minimal.coverage> + </properties> </project> diff --git a/samples/pom.xml b/samples/pom.xml index 5ff6764..e57150a 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -20,4 +20,7 @@ <version>${project.version}</version> </dependency> </dependencies> + <properties> + <minimal.coverage>0.0</minimal.coverage> + </properties> </project> diff --git a/samples/src/main/java/fi/iki/elonen/HelloServer.java b/samples/src/main/java/fi/iki/elonen/HelloServer.java index 7226108..bc91231 100644 --- a/samples/src/main/java/fi/iki/elonen/HelloServer.java +++ b/samples/src/main/java/fi/iki/elonen/HelloServer.java @@ -36,6 +36,8 @@ package fi.iki.elonen; import java.util.Map; import java.util.logging.Logger; +import fi.iki.elonen.util.ServerRunner; + /** * An example of subclassing NanoHTTPD to make a custom HTTP server. */ diff --git a/samples/src/main/java/fi/iki/elonen/TempFilesServer.java b/samples/src/main/java/fi/iki/elonen/TempFilesServer.java index dbe7d37..8429e32 100644 --- a/samples/src/main/java/fi/iki/elonen/TempFilesServer.java +++ b/samples/src/main/java/fi/iki/elonen/TempFilesServer.java @@ -37,6 +37,7 @@ import java.util.ArrayList; import java.util.List; import fi.iki.elonen.debug.DebugServer; +import fi.iki.elonen.util.ServerRunner; /** * @author Paul S. Hawke (paul.hawke@gmail.com) On: 3/9/13 at 12:47 AM diff --git a/samples/src/main/java/fi/iki/elonen/debug/DebugServer.java b/samples/src/main/java/fi/iki/elonen/debug/DebugServer.java index 056673d..0ffc34f 100644 --- a/samples/src/main/java/fi/iki/elonen/debug/DebugServer.java +++ b/samples/src/main/java/fi/iki/elonen/debug/DebugServer.java @@ -38,7 +38,7 @@ import java.util.List; import java.util.Map; import fi.iki.elonen.NanoHTTPD; -import fi.iki.elonen.ServerRunner; +import fi.iki.elonen.util.ServerRunner; public class DebugServer extends NanoHTTPD { diff --git a/webserver/pom.xml b/webserver/pom.xml index 32b1f54..3ecfd72 100644 --- a/webserver/pom.xml +++ b/webserver/pom.xml @@ -4,7 +4,7 @@ <parent> <groupId>org.nanohttpd</groupId> <artifactId>nanohttpd-project</artifactId> - <version>2.2.1-SNAPSHOT</version> + <version>2.2.0-SNAPSHOT</version> </parent> <artifactId>nanohttpd-webserver</artifactId> <packaging>jar</packaging> @@ -49,4 +49,7 @@ </plugin> </plugins> </build> + <properties> + <minimal.coverage>0.74</minimal.coverage> + </properties> </project> diff --git a/webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java b/webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java index 0ddc21c..f5415d6 100644 --- a/webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java +++ b/webserver/src/main/java/fi/iki/elonen/SimpleWebServer.java @@ -32,7 +32,6 @@ package fi.iki.elonen; * OF THE POSSIBILITY OF SUCH DAMAGE. * #L% */ - import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -51,6 +50,7 @@ import java.util.ServiceLoader; import java.util.StringTokenizer; import fi.iki.elonen.NanoHTTPD.Response.IStatus; +import fi.iki.elonen.util.ServerRunner; public class SimpleWebServer extends NanoHTTPD { @@ -137,6 +137,7 @@ public class SimpleWebServer extends NanoHTTPD { String host = null; // bind to all interfaces by default List<File> rootDirs = new ArrayList<File>(); boolean quiet = false; + String cors = null; Map<String, String> options = new HashMap<String, String>(); // Parse command-line, with short and long versions of the options. @@ -149,6 +150,12 @@ public class SimpleWebServer extends NanoHTTPD { quiet = true; } else if (args[i].equalsIgnoreCase("-d") || args[i].equalsIgnoreCase("--dir")) { rootDirs.add(new File(args[i + 1]).getAbsoluteFile()); + } else if (args[i].startsWith("--cors")) { + cors = "*"; + int equalIdx = args[i].indexOf('='); + if (equalIdx > 0) { + cors = args[i].substring(equalIdx + 1); + } } else if (args[i].equalsIgnoreCase("--licence")) { System.out.println(SimpleWebServer.LICENCE + "\n"); } else if (args[i].startsWith("-X:")) { @@ -164,7 +171,6 @@ public class SimpleWebServer extends NanoHTTPD { if (rootDirs.isEmpty()) { rootDirs.add(new File(".").getAbsoluteFile()); } - options.put("host", host); options.put("port", "" + port); options.put("quiet", String.valueOf(quiet)); @@ -179,7 +185,6 @@ public class SimpleWebServer extends NanoHTTPD { } } options.put("home", sb.toString()); - ServiceLoader<WebServerPluginInfo> serviceLoader = ServiceLoader.load(WebServerPluginInfo.class); for (WebServerPluginInfo info : serviceLoader) { String[] mimeTypes = info.getMimeTypes(); @@ -198,8 +203,7 @@ public class SimpleWebServer extends NanoHTTPD { registerPluginForMimeType(indexFiles, mime, info.getWebServerPlugin(mime), options); } } - - ServerRunner.executeInstance(new SimpleWebServer(host, port, rootDirs, quiet)); + ServerRunner.executeInstance(new SimpleWebServer(host, port, rootDirs, quiet, cors)); } protected static void registerPluginForMimeType(String[] indexFiles, String mimeType, WebServerPlugin plugin, Map<String, String> commandLineOptions) { @@ -223,20 +227,26 @@ public class SimpleWebServer extends NanoHTTPD { private final boolean quiet; + private final String cors; + protected List<File> rootDirs; - public SimpleWebServer(String host, int port, File wwwroot, boolean quiet) { - super(host, port); - this.quiet = quiet; - this.rootDirs = new ArrayList<File>(); - this.rootDirs.add(wwwroot); + public SimpleWebServer(String host, int port, File wwwroot, boolean quiet, String cors) { + this(host, port, Collections.singletonList(wwwroot), quiet, cors); + } - init(); + public SimpleWebServer(String host, int port, File wwwroot, boolean quiet) { + this(host, port, Collections.singletonList(wwwroot), quiet, null); } public SimpleWebServer(String host, int port, List<File> wwwroots, boolean quiet) { + this(host, port, wwwroots, quiet, null); + } + + public SimpleWebServer(String host, int port, List<File> wwwroots, boolean quiet, String cors) { super(host, port); this.quiet = quiet; + this.cors = cors; this.rootDirs = new ArrayList<File>(wwwroots); init(); @@ -393,6 +403,21 @@ public class SimpleWebServer extends NanoHTTPD { } private Response respond(Map<String, String> headers, IHTTPSession session, String uri) { + // First let's handle CORS OPTION query + Response r; + if (cors != null && Method.OPTIONS.equals(session.getMethod())) { + r = new NanoHTTPD.Response(Response.Status.OK, MIME_PLAINTEXT, null, 0); + } else { + r = defaultRespond(headers, session, uri); + } + + if (cors != null) { + r = addCORSHeaders(headers, r, cors); + } + return r; + } + + private Response defaultRespond(Map<String, String> headers, IHTTPSession session, String uri) { // Remove URL arguments uri = uri.trim().replace(File.separatorChar, '/'); if (uri.indexOf('?') >= 0) { @@ -595,4 +620,31 @@ public class SimpleWebServer extends NanoHTTPD { res.addHeader("Accept-Ranges", "bytes"); return res; } + + protected Response addCORSHeaders(Map<String, String> queryHeaders, Response resp, String cors) { + resp.addHeader("Access-Control-Allow-Origin", cors); + resp.addHeader("Access-Control-Allow-Headers", calculateAllowHeaders(queryHeaders)); + resp.addHeader("Access-Control-Allow-Credentials", "true"); + resp.addHeader("Access-Control-Allow-Methods", ALLOWED_METHODS); + resp.addHeader("Access-Control-Max-Age", "" + MAX_AGE); + + return resp; + } + + private String calculateAllowHeaders(Map<String, String> queryHeaders) { + // here we should use the given asked headers + // but NanoHttpd uses a Map whereas it is possible for requester to send + // several time the same header + // let's just use default values for this version + return System.getProperty(ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME, DEFAULT_ALLOWED_HEADERS); + } + + private final static String ALLOWED_METHODS = "GET, POST, PUT, DELETE, OPTIONS, HEAD"; + + private final static int MAX_AGE = 42 * 60 * 60; + + // explicitly relax visibility to package for tests purposes + final static String DEFAULT_ALLOWED_HEADERS = "origin,accept,content-type"; + + public final static String ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME = "AccessControlAllowHeader"; } diff --git a/webserver/src/test/java/fi/iki/elonen/AbstractTestHttpServer.java b/webserver/src/test/java/fi/iki/elonen/AbstractTestHttpServer.java new file mode 100644 index 0000000..b56c2b0 --- /dev/null +++ b/webserver/src/test/java/fi/iki/elonen/AbstractTestHttpServer.java @@ -0,0 +1,67 @@ +package fi.iki.elonen; + +/* + * #%L + * NanoHttpd-Webserver + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * 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, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd 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 HOLDER 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. + * #L% + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import org.apache.http.HttpEntity; + +/** + * @author Matthieu Brouillard [matthieu@brouillard.fr] + */ +public class AbstractTestHttpServer { + + protected byte[] readContents(HttpEntity entity) throws IOException { + InputStream instream = entity.getContent(); + return readContents(instream); + } + + protected byte[] readContents(InputStream instream) throws IOException { + byte[] bytes; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + byte[] buffer = new byte[1024]; + int count; + while ((count = instream.read(buffer)) >= 0) { + out.write(buffer, 0, count); + } + bytes = out.toByteArray(); + } finally { + instream.close(); + } + return bytes; + } + +} diff --git a/webserver/src/test/java/fi/iki/elonen/TestCorsHttpServer.java b/webserver/src/test/java/fi/iki/elonen/TestCorsHttpServer.java new file mode 100644 index 0000000..93f4699 --- /dev/null +++ b/webserver/src/test/java/fi/iki/elonen/TestCorsHttpServer.java @@ -0,0 +1,154 @@ +package fi.iki.elonen; + +/* + * #%L + * NanoHttpd-Webserver + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * 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, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd 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 HOLDER 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. + * #L% + */ + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; + +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * @author Matthieu Brouillard [matthieu@brouillard.fr] + */ +public class TestCorsHttpServer extends AbstractTestHttpServer { + + private static PipedOutputStream stdIn; + + private static Thread serverStartThread; + + @BeforeClass + public static void setUp() throws Exception { + stdIn = new PipedOutputStream(); + System.setIn(new PipedInputStream(stdIn)); + serverStartThread = new Thread(new Runnable() { + + @Override + public void run() { + String[] args = { + "--host", + "localhost", + "--port", + "9090", + "--dir", + "src/test/resources", + "--cors" + }; + SimpleWebServer.main(args); + } + }); + serverStartThread.start(); + // give the server some tine to start. + Thread.sleep(100); + } + + @AfterClass + public static void tearDown() throws Exception { + stdIn.write("\n\n".getBytes()); + serverStartThread.join(2000); + Assert.assertFalse(serverStartThread.isAlive()); + } + + @Test + public void doTestOption() throws Exception { + CloseableHttpClient httpclient = HttpClients.createDefault(); + HttpOptions httpOption = new HttpOptions("http://localhost:9090/xxx/yyy.html"); + CloseableHttpResponse response = httpclient.execute(httpOption); + Assert.assertEquals(200, response.getStatusLine().getStatusCode()); + Assert.assertNotNull("Cors should have added a header: Access-Control-Allow-Origin", response.getLastHeader("Access-Control-Allow-Origin")); + Assert.assertEquals("Cors should have added a header: Access-Control-Allow-Origin: *", "*", response.getLastHeader("Access-Control-Allow-Origin").getValue()); + response.close(); + } + + @Test + public void doSomeBasicTest() throws Exception { + CloseableHttpClient httpclient = HttpClients.createDefault(); + HttpGet httpget = new HttpGet("http://localhost:9090/testdir/test.html"); + CloseableHttpResponse response = httpclient.execute(httpget); + HttpEntity entity = response.getEntity(); + String string = new String(readContents(entity), "UTF-8"); + + Assert.assertNotNull("Cors should have added a header: Access-Control-Allow-Origin", response.getLastHeader("Access-Control-Allow-Origin")); + Assert.assertEquals("Cors should have added a header: Access-Control-Allow-Origin: *", "*", response.getLastHeader("Access-Control-Allow-Origin").getValue()); + Assert.assertEquals("<html>\n<head>\n<title>dummy</title>\n</head>\n<body>\n\t<h1>it works</h1>\n</body>\n</html>", string); + response.close(); + } + + @Test + public void testAccessControlAllowHeaderUsesDefaultsWithoutSystemProperty() throws Exception { + Assert.assertNull("no System " + SimpleWebServer.ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME + " shoudl be set", + System.getProperty(SimpleWebServer.ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME)); + + CloseableHttpClient httpclient = HttpClients.createDefault(); + HttpOptions httpOption = new HttpOptions("http://localhost:9090/xxx/yyy.html"); + CloseableHttpResponse response = httpclient.execute(httpOption); + Assert.assertEquals(200, response.getStatusLine().getStatusCode()); + Assert.assertEquals("Cors should have added a header: Access-Control-Allow-Headers: " + SimpleWebServer.DEFAULT_ALLOWED_HEADERS, + SimpleWebServer.DEFAULT_ALLOWED_HEADERS, response.getLastHeader("Access-Control-Allow-Headers").getValue()); + response.close(); + } + + @Test + public void testAccessControlAllowHeaderUsesSystemPropertyWhenSet() throws Exception { + Assert.assertNull("no System " + SimpleWebServer.ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME + " shoudl be set", + System.getProperty(SimpleWebServer.ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME)); + + final String expectedValue = "origin"; + System.setProperty(SimpleWebServer.ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME, expectedValue); + + try { + CloseableHttpClient httpclient = HttpClients.createDefault(); + HttpOptions httpOption = new HttpOptions("http://localhost:9090/xxx/yyy.html"); + CloseableHttpResponse response = httpclient.execute(httpOption); + Assert.assertEquals(200, response.getStatusLine().getStatusCode()); + Assert.assertEquals("Cors should have added a header: Access-Control-Allow-Headers: " + expectedValue, expectedValue, + response.getLastHeader("Access-Control-Allow-Headers").getValue()); + response.close(); + } finally { + System.clearProperty(SimpleWebServer.ACCESS_CONTROL_ALLOW_HEADER_PROPERTY_NAME); + } + } +} diff --git a/webserver/src/test/java/fi/iki/elonen/TestCorsHttpServerWithSingleOrigin.java b/webserver/src/test/java/fi/iki/elonen/TestCorsHttpServerWithSingleOrigin.java new file mode 100644 index 0000000..dbd2c4e --- /dev/null +++ b/webserver/src/test/java/fi/iki/elonen/TestCorsHttpServerWithSingleOrigin.java @@ -0,0 +1,121 @@ +package fi.iki.elonen; + +/* + * #%L + * NanoHttpd-Webserver + * %% + * Copyright (C) 2012 - 2015 nanohttpd + * %% + * 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, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the nanohttpd 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 HOLDER 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. + * #L% + */ + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; + +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * @author Matthieu Brouillard [matthieu@brouillard.fr] + */ +public class TestCorsHttpServerWithSingleOrigin extends AbstractTestHttpServer { + + private static PipedOutputStream stdIn; + + private static Thread serverStartThread; + + @BeforeClass + public static void setUp() throws Exception { + stdIn = new PipedOutputStream(); + System.setIn(new PipedInputStream(stdIn)); + serverStartThread = new Thread(new Runnable() { + + @Override + public void run() { + String[] args = { + "--host", + "localhost", + "--port", + "9090", + "--dir", + "src/test/resources", + "--cors=http://localhost:9090" + }; + SimpleWebServer.main(args); + } + }); + serverStartThread.start(); + // give the server some tine to start. + Thread.sleep(100); + } + + @AfterClass + public static void tearDown() throws Exception { + stdIn.write("\n\n".getBytes()); + serverStartThread.join(2000); + Assert.assertFalse(serverStartThread.isAlive()); + } + + @Test + public void doTestOption() throws Exception { + CloseableHttpClient httpclient = HttpClients.createDefault(); + HttpOptions httpOption = new HttpOptions("http://localhost:9090/xxx/yyy.html"); + CloseableHttpResponse response = httpclient.execute(httpOption); + Assert.assertEquals(200, response.getStatusLine().getStatusCode()); + Assert.assertNotNull("Cors should have added a header: Access-Control-Allow-Origin", response.getLastHeader("Access-Control-Allow-Origin")); + Assert.assertEquals("Cors should have added a header: Access-Control-Allow-Origin: http://localhost:9090", "http://localhost:9090", + response.getLastHeader("Access-Control-Allow-Origin").getValue()); + response.close(); + } + + @Test + public void doSomeBasicTest() throws Exception { + CloseableHttpClient httpclient = HttpClients.createDefault(); + HttpGet httpget = new HttpGet("http://localhost:9090/testdir/test.html"); + CloseableHttpResponse response = httpclient.execute(httpget); + HttpEntity entity = response.getEntity(); + String string = new String(readContents(entity), "UTF-8"); + + Assert.assertNotNull("Cors should have added a header: Access-Control-Allow-Origin", response.getLastHeader("Access-Control-Allow-Origin")); + Assert.assertEquals("Cors should have added a header: Access-Control-Allow-Origin: http://localhost:9090", "http://localhost:9090", + response.getLastHeader("Access-Control-Allow-Origin").getValue()); + Assert.assertEquals("<html>\n<head>\n<title>dummy</title>\n</head>\n<body>\n\t<h1>it works</h1>\n</body>\n</html>", string); + response.close(); + } +} diff --git a/webserver/src/test/java/fi/iki/elonen/TestHttpServer.java b/webserver/src/test/java/fi/iki/elonen/TestHttpServer.java index aa08f49..bb16741 100644 --- a/webserver/src/test/java/fi/iki/elonen/TestHttpServer.java +++ b/webserver/src/test/java/fi/iki/elonen/TestHttpServer.java @@ -50,7 +50,7 @@ import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; -public class TestHttpServer { +public class TestHttpServer extends AbstractTestHttpServer { private static PipedOutputStream stdIn; @@ -146,27 +146,4 @@ public class TestHttpServer { response.close(); } - - private byte[] readContents(HttpEntity entity) throws IOException { - InputStream instream = entity.getContent(); - return readContents(instream); - } - - private byte[] readContents(InputStream instream) throws IOException { - byte[] bytes; - ByteArrayOutputStream out = new ByteArrayOutputStream(); - - try { - byte[] buffer = new byte[1024]; - int count; - while ((count = instream.read(buffer)) >= 0) { - out.write(buffer, 0, count); - } - bytes = out.toByteArray(); - } finally { - instream.close(); - } - return bytes; - } - } diff --git a/websocket/pom.xml b/websocket/pom.xml index 55d2fde..4cb99a1 100644 --- a/websocket/pom.xml +++ b/websocket/pom.xml @@ -62,4 +62,7 @@ <scope>test</scope> </dependency> </dependencies> + <properties> + <minimal.coverage>0.67</minimal.coverage> + </properties> </project> diff --git a/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java b/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java index e23abe2..73e5f67 100644 --- a/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java +++ b/websocket/src/main/java/fi/iki/elonen/NanoWebSocketServer.java @@ -96,13 +96,13 @@ public abstract class NanoWebSocketServer extends NanoHTTPD { this.handshakeResponse.addHeader(NanoWebSocketServer.HEADER_CONNECTION, NanoWebSocketServer.HEADER_CONNECTION_VALUE); } - public void close(CloseCode code, String reason) throws IOException { + public void close(CloseCode code, String reason, boolean initiatedByRemote) throws IOException { State oldState = this.state; this.state = State.CLOSING; if (oldState == State.OPEN) { sendFrame(new CloseFrame(code, reason)); } else { - doClose(code, reason, false); + doClose(code, reason, initiatedByRemote); } } @@ -149,13 +149,7 @@ public abstract class NanoWebSocketServer extends NanoHTTPD { // Answer for my requested close doClose(code, reason, false); } else { - // Answer close request from other endpoint and close self - State oldState = this.state; - this.state = State.CLOSING; - if (oldState == State.OPEN) { - sendFrame(new CloseFrame(code, reason)); - } - doClose(code, reason, true); + close(code, reason, true); } } diff --git a/websocket/src/test/java/fi/iki/elonen/samples/echo/EchoWebSocketsTest.java b/websocket/src/test/java/fi/iki/elonen/samples/echo/EchoWebSocketsTest.java index 6061023..e90ae71 100644 --- a/websocket/src/test/java/fi/iki/elonen/samples/echo/EchoWebSocketsTest.java +++ b/websocket/src/test/java/fi/iki/elonen/samples/echo/EchoWebSocketsTest.java @@ -68,6 +68,8 @@ public class EchoWebSocketsTest { SimpleEchoSocket socket = new SimpleEchoSocket(); socket.getToSendMessages().add("Hello"); socket.getToSendMessages().add("Thanks for the conversation."); + socket.getToSendMessages().add(createString(31000)); + socket.getToSendMessages().add(createString(65400)); try { client.start(); URI echoUri = new URI(destUri); @@ -84,9 +86,17 @@ public class EchoWebSocketsTest { e.printStackTrace(); } } - Assert.assertEquals(2, socket.getReceivedMessages().size()); + Assert.assertEquals(4, socket.getReceivedMessages().size()); Assert.assertEquals("Hello", socket.getReceivedMessages().get(0)); Assert.assertEquals("Thanks for the conversation.", socket.getReceivedMessages().get(1)); } + + private String createString(int i) { + StringBuilder builder = new StringBuilder(); + while (builder.length() < i) { + builder.append("A very long text."); + } + return builder.toString(); + } } |