diff options
author | Richard van Nieuwenhoven <richard.vannieuwenhoven@adesso.at> | 2015-05-23 18:14:47 +0200 |
---|---|---|
committer | Richard van Nieuwenhoven <richard.vannieuwenhoven@adesso.at> | 2015-05-23 18:14:47 +0200 |
commit | 9628584ddde247fda914fd50f2d60362e53cea9f (patch) | |
tree | 5e782b38ea80cd12870d0a60af442eea76ef7031 /core/src | |
parent | 284d64856c4ab6629228cfdb5437e54d0bd0bf7a (diff) | |
parent | a45eb0e96d8081f67baca79660ba44aa01978b88 (diff) | |
download | nanohttpd-9628584ddde247fda914fd50f2d60362e53cea9f.tar.gz |
Merge pull request #184 from AlbinTheander/gzip
Added support for gzip
Diffstat (limited to 'core/src')
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())); + + } + +} |