aboutsummaryrefslogtreecommitdiff
path: root/core/src
diff options
context:
space:
mode:
authorRichard van Nieuwenhoven <richard.vannieuwenhoven@adesso.at>2015-05-23 18:14:47 +0200
committerRichard van Nieuwenhoven <richard.vannieuwenhoven@adesso.at>2015-05-23 18:14:47 +0200
commit9628584ddde247fda914fd50f2d60362e53cea9f (patch)
tree5e782b38ea80cd12870d0a60af442eea76ef7031 /core/src
parent284d64856c4ab6629228cfdb5437e54d0bd0bf7a (diff)
parenta45eb0e96d8081f67baca79660ba44aa01978b88 (diff)
downloadnanohttpd-9628584ddde247fda914fd50f2d60362e53cea9f.tar.gz
Merge pull request #184 from AlbinTheander/gzip
Added support for gzip
Diffstat (limited to 'core/src')
-rw-r--r--core/src/main/java/fi/iki/elonen/NanoHTTPD.java151
-rw-r--r--core/src/test/java/fi/iki/elonen/HttpChunkedResponseTest.java2
-rw-r--r--core/src/test/java/fi/iki/elonen/integration/GZipIntegrationTest.java156
3 files changed, 263 insertions, 46 deletions
diff --git a/core/src/main/java/fi/iki/elonen/NanoHTTPD.java b/core/src/main/java/fi/iki/elonen/NanoHTTPD.java
index 90fa479..de4bc83 100644
--- a/core/src/main/java/fi/iki/elonen/NanoHTTPD.java
+++ b/core/src/main/java/fi/iki/elonen/NanoHTTPD.java
@@ -33,22 +33,7 @@ package fi.iki.elonen;
* #L%
*/
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.ByteArrayInputStream;
-import java.io.Closeable;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-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.io.*;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
@@ -73,6 +58,7 @@ import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;
+import java.util.zip.GZIPOutputStream;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
@@ -735,8 +721,10 @@ public abstract class NanoHTTPD {
if (r == null) {
throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
} else {
+ String acceptEncoding = this.headers.get("accept-encoding");
this.cookies.unloadQueue(r);
r.setRequestMethod(this.method);
+ r.setGzipEncoding(acceptEncoding != null && acceptEncoding.contains("gzip"));
r.send(this.outputStream);
}
} catch (SocketException e) {
@@ -1106,6 +1094,46 @@ public abstract class NanoHTTPD {
public int getRequestStatus() {
return this.requestStatus;
}
+
+ }
+
+ /**
+ * Output stream that will automatically send every write to the wrapped
+ * OutputStream according to chunked transfer:
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
+ */
+ private static class ChunkedOutputStream extends FilterOutputStream {
+
+ public ChunkedOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ byte[] data = {
+ (byte) b
+ };
+ write(data, 0, 1);
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ if (len == 0)
+ return;
+ out.write(String.format("%x\r\n", len).getBytes());
+ out.write(b, off, len);
+ out.write("\r\n".getBytes());
+ }
+
+ public void finish() throws IOException {
+ out.write("0\r\n\r\n".getBytes());
+ }
+
}
/**
@@ -1140,6 +1168,8 @@ public abstract class NanoHTTPD {
*/
private boolean chunkedTransfer;
+ private boolean encodeAsGzip;
+
/**
* Creates a fixed length response if totalBytes>=0, otherwise chunked.
*/
@@ -1183,6 +1213,10 @@ public abstract class NanoHTTPD {
return this.status;
}
+ public void setGzipEncoding(boolean encodeAsGzip) {
+ this.encodeAsGzip = encodeAsGzip;
+ }
+
private boolean headerAlreadySent(Map<String, String> header, String name) {
boolean alreadySent = false;
for (String headerName : header.keySet()) {
@@ -1223,15 +1257,23 @@ public abstract class NanoHTTPD {
sendConnectionHeaderIfNotAlreadyPresent(pw, this.header);
+ if (headerAlreadySent(this.header, "content-length")) {
+ encodeAsGzip = false;
+ }
+
+ if (encodeAsGzip) {
+ pw.print("Content-Encoding: gzip\r\n");
+ }
+
+ long pending = this.data != null ? this.contentLength : 0;
if (this.requestMethod != Method.HEAD && this.chunkedTransfer) {
- sendAsChunked(outputStream, pw);
- } else {
- long pending = this.data != null ? this.contentLength : 0;
+ pw.print("Transfer-Encoding: chunked\r\n");
+ } else if (!encodeAsGzip) {
pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, this.header, pending);
- pw.print("\r\n");
- pw.flush();
- sendAsFixedLength(outputStream, pending);
}
+ pw.print("\r\n");
+ pw.flush();
+ sendBodyWithCorrectTransferAndEncoding(outputStream, pending);
outputStream.flush();
safeClose(this.data);
} catch (IOException ioe) {
@@ -1239,32 +1281,51 @@ public abstract class NanoHTTPD {
}
}
- private void sendAsChunked(OutputStream outputStream, PrintWriter pw) throws IOException {
- pw.print("Transfer-Encoding: chunked\r\n");
- pw.print("\r\n");
- pw.flush();
- int BUFFER_SIZE = 16 * 1024;
- byte[] CRLF = "\r\n".getBytes();
- byte[] buff = new byte[BUFFER_SIZE];
- int read;
- while ((read = this.data.read(buff)) > 0) {
- outputStream.write(String.format("%x\r\n", read).getBytes());
- outputStream.write(buff, 0, read);
- outputStream.write(CRLF);
+ private void sendBodyWithCorrectTransferAndEncoding(OutputStream outputStream, long pending) throws IOException {
+ if (this.requestMethod != Method.HEAD && this.chunkedTransfer) {
+ ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream);
+ sendBodyWithCorrectEncoding(chunkedOutputStream, -1);
+ chunkedOutputStream.finish();
+ } else {
+ sendBodyWithCorrectEncoding(outputStream, pending);
}
- outputStream.write(String.format("0\r\n\r\n").getBytes());
}
- private void sendAsFixedLength(OutputStream outputStream, long pending) throws IOException {
- if (this.requestMethod != Method.HEAD && this.data != null) {
- long BUFFER_SIZE = 16 * 1024;
- byte[] buff = new byte[(int) BUFFER_SIZE];
- while (pending > 0) {
- int read = this.data.read(buff, 0, (int) (pending > BUFFER_SIZE ? BUFFER_SIZE : pending));
- if (read <= 0) {
- break;
- }
- outputStream.write(buff, 0, read);
+ private void sendBodyWithCorrectEncoding(OutputStream outputStream, long pending) throws IOException {
+ if (encodeAsGzip) {
+ GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream, true);
+ sendBody(gzipOutputStream, -1);
+ gzipOutputStream.finish();
+ } else {
+ sendBody(outputStream, pending);
+ }
+ }
+
+ /**
+ * Sends the body to the specified OutputStream. The pending parameter
+ * limits the maximum amounts of bytes sent unless it is -1, in which
+ * case everything is sent.
+ *
+ * @param outputStream
+ * the OutputStream to send data to
+ * @param pending
+ * -1 to send everything, otherwise sets a max limit to the
+ * number of bytes sent
+ * @throws IOException
+ * if something goes wrong while sending the data.
+ */
+ private void sendBody(OutputStream outputStream, long pending) throws IOException {
+ long BUFFER_SIZE = 16 * 1024;
+ byte[] buff = new byte[(int) BUFFER_SIZE];
+ boolean sendEverything = pending == -1;
+ while (pending > 0 || sendEverything) {
+ long bytesToRead = sendEverything ? BUFFER_SIZE : Math.min(pending, BUFFER_SIZE);
+ int read = this.data.read(buff, 0, (int) bytesToRead);
+ if (read <= 0) {
+ break;
+ }
+ outputStream.write(buff, 0, read);
+ if (!sendEverything) {
pending -= read;
}
}
diff --git a/core/src/test/java/fi/iki/elonen/HttpChunkedResponseTest.java b/core/src/test/java/fi/iki/elonen/HttpChunkedResponseTest.java
index 5d06ae4..8853efa 100644
--- a/core/src/test/java/fi/iki/elonen/HttpChunkedResponseTest.java
+++ b/core/src/test/java/fi/iki/elonen/HttpChunkedResponseTest.java
@@ -52,7 +52,7 @@ public class HttpChunkedResponseTest extends HttpServerTest {
}
@Override
- public synchronized int read(byte[] buffer) throws IOException {
+ public synchronized int read(byte[] buffer, int off, int len) throws IOException {
// Too implementation-linked, but...
for (int i = 0; i < this.chunks[this.chunk].length(); ++i) {
buffer[i] = (byte) this.chunks[this.chunk].charAt(i);
diff --git a/core/src/test/java/fi/iki/elonen/integration/GZipIntegrationTest.java b/core/src/test/java/fi/iki/elonen/integration/GZipIntegrationTest.java
new file mode 100644
index 0000000..e1d92d7
--- /dev/null
+++ b/core/src/test/java/fi/iki/elonen/integration/GZipIntegrationTest.java
@@ -0,0 +1,156 @@
+package fi.iki.elonen.integration;
+
+/*
+ * #%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 fi.iki.elonen.NanoHTTPD;
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DecompressingHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.junit.Test;
+
+import java.io.*;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+public class GZipIntegrationTest extends IntegrationTestBase<GZipIntegrationTest.TestServer> {
+
+ public static class TestServer extends NanoHTTPD {
+
+ public Response response;
+
+ public TestServer() {
+ super(8192);
+ }
+
+ @Override
+ public Response serve(IHTTPSession session) {
+ return response;
+ }
+ }
+
+ @Override
+ public TestServer createTestServer() {
+ return new TestServer();
+ }
+
+ @Test
+ public void contentEncodingShouldBeAddedToFixedLengthResponses() throws IOException {
+ testServer.response = testServer.newFixedLengthResponse("This is a test");
+ HttpGet request = new HttpGet("http://localhost:8192/");
+ request.addHeader("Accept-encoding", "gzip");
+ HttpResponse response = httpclient.execute(request);
+ Header contentEncoding = response.getFirstHeader("content-encoding");
+ assertNotNull("Content-Encoding should be set", contentEncoding);
+ assertEquals("gzip", contentEncoding.getValue());
+ }
+
+ @Test
+ public void contentEncodingShouldBeAddedToChunkedResponses() throws IOException {
+ InputStream data = new ByteArrayInputStream("This is a test".getBytes("UTF-8"));
+ testServer.response = testServer.newChunkedResponse(NanoHTTPD.Response.Status.OK, "text/plain", data);
+ HttpGet request = new HttpGet("http://localhost:8192/");
+ request.addHeader("Accept-encoding", "gzip");
+ HttpResponse response = httpclient.execute(request);
+ Header contentEncoding = response.getFirstHeader("content-encoding");
+ assertNotNull("Content-Encoding should be set", contentEncoding);
+ assertEquals("gzip", contentEncoding.getValue());
+ }
+
+ @Test
+ public void shouldFindCorrectAcceptEncodingAmongMany() throws IOException {
+ testServer.response = testServer.newFixedLengthResponse("This is a test");
+ HttpGet request = new HttpGet("http://localhost:8192/");
+ request.addHeader("Accept-encoding", "deflate,gzip");
+ HttpResponse response = httpclient.execute(request);
+ Header contentEncoding = response.getFirstHeader("content-encoding");
+ assertNotNull("Content-Encoding should be set", contentEncoding);
+ assertEquals("gzip", contentEncoding.getValue());
+ }
+
+ @Test
+ public void contentLengthShouldBeRemovedFromZippedResponses() throws IOException {
+ testServer.response = testServer.newFixedLengthResponse("This is a test");
+ HttpGet request = new HttpGet("http://localhost:8192/");
+ request.addHeader("Accept-encoding", "gzip");
+ HttpResponse response = httpclient.execute(request);
+ Header contentLength = response.getFirstHeader("content-length");
+ assertNull("Content-Length should not be set when gzipping response", contentLength);
+ }
+
+ @Test
+ public void fixedLengthContentIsEncodedProperly() throws IOException {
+ testServer.response = testServer.newFixedLengthResponse("This is a test");
+ HttpGet request = new HttpGet("http://localhost:8192/");
+ request.addHeader("Accept-encoding", "gzip");
+ HttpResponse response = new DecompressingHttpClient(httpclient).execute(request);
+ assertEquals("This is a test", EntityUtils.toString(response.getEntity()));
+ }
+
+ @Test
+ public void chunkedContentIsEncodedProperly() throws IOException {
+ InputStream data = new ByteArrayInputStream("This is a test".getBytes("UTF-8"));
+ testServer.response = testServer.newChunkedResponse(NanoHTTPD.Response.Status.OK, "text/plain", data);
+ HttpGet request = new HttpGet("http://localhost:8192/");
+ request.addHeader("Accept-encoding", "gzip");
+ HttpResponse response = new DecompressingHttpClient(httpclient).execute(request);
+ assertEquals("This is a test", EntityUtils.toString(response.getEntity()));
+ }
+
+ @Test
+ public void noGzipWithoutAcceptEncoding() throws IOException {
+ testServer.response = testServer.newFixedLengthResponse("This is a test");
+ HttpGet request = new HttpGet("http://localhost:8192/");
+ HttpResponse response = httpclient.execute(request);
+ Header contentEncoding = response.getFirstHeader("content-encoding");
+ assertThat(contentEncoding, is(nullValue()));
+ assertEquals("This is a test", EntityUtils.toString(response.getEntity()));
+ }
+
+ @Test
+ public void contentShouldNotBeGzippedIfContentLengthIsAddedManually() throws IOException {
+ testServer.response = testServer.newFixedLengthResponse("This is a test");
+ testServer.response.addHeader("Content-Length", "" + ("This is a test".getBytes("UTF-8").length));
+ HttpGet request = new HttpGet("http://localhost:8192/");
+ request.addHeader("Accept-encoding", "gzip");
+ HttpResponse response = httpclient.execute(request);
+ Header contentEncoding = response.getFirstHeader("content-encoding");
+ assertNull("Content-Encoding should not be set when manually setting content-length", contentEncoding);
+ assertEquals("This is a test", EntityUtils.toString(response.getEntity()));
+
+ }
+
+}