diff options
author | Simone Bordet | 2015-09-21 13:17:36 +0000 |
---|---|---|
committer | Simone Bordet | 2015-09-21 13:54:25 +0000 |
commit | 144da724c991689f00e80b36fde8d28fdffd4e6c (patch) | |
tree | 679262162accf66b74dbe79fef233c0d40a0abdf | |
parent | 01ef307198c4feb0e842c9ce067c1f1669412b45 (diff) | |
download | org.eclipse.jetty.project-144da724c991689f00e80b36fde8d28fdffd4e6c.tar.gz org.eclipse.jetty.project-144da724c991689f00e80b36fde8d28fdffd4e6c.tar.xz org.eclipse.jetty.project-144da724c991689f00e80b36fde8d28fdffd4e6c.zip |
477948 - Connections leaked when response contains Connection: close header.
Made HttpParser.isComplete() return true if it's a response and it's
in SEEKING_EOF state.
3 files changed, 384 insertions, 13 deletions
diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ServerConnectionCloseTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ServerConnectionCloseTest.java new file mode 100644 index 0000000000..07dc291abe --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ServerConnectionCloseTest.java @@ -0,0 +1,168 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client; + +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.toolchain.test.TestTracker; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.After; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; + +public class ServerConnectionCloseTest +{ + @Rule + public final TestTracker tracker = new TestTracker(); + private HttpClient client; + + private void startClient() throws Exception + { + QueuedThreadPool clientThreads = new QueuedThreadPool(); + clientThreads.setName("client"); + client = new HttpClient(); + client.setThreadPool(clientThreads); + client.start(); + } + + @After + public void dispose() throws Exception + { + if (client != null) + client.stop(); + } + + @Test + public void testServerSendsConnectionCloseWithoutContent() throws Exception + { + testServerSendsConnectionClose(true, false, ""); + } + + @Test + public void testServerSendsConnectionCloseWithContent() throws Exception + { + testServerSendsConnectionClose(true, false, "data"); + } + + @Test + public void testServerSendsConnectionCloseWithChunkedContent() throws Exception + { + testServerSendsConnectionClose(true, true, "data"); + } + + @Test + public void testServerSendsConnectionCloseWithoutContentButDoesNotClose() throws Exception + { + testServerSendsConnectionClose(false, false, ""); + } + + @Test + public void testServerSendsConnectionCloseWithContentButDoesNotClose() throws Exception + { + testServerSendsConnectionClose(false, false, "data"); + } + + @Test + public void testServerSendsConnectionCloseWithChunkedContentButDoesNotClose() throws Exception + { + testServerSendsConnectionClose(false, true, "data"); + } + + private void testServerSendsConnectionClose(boolean shutdownOutput, boolean chunked, String content) throws Exception + { + ServerSocket server = new ServerSocket(0); + int port = server.getLocalPort(); + + startClient(); + + ContentExchange exchange = new ContentExchange(true); + exchange.setURL("http://localhost:" + port + "/ctx/path"); + client.send(exchange); + + Socket socket = server.accept(); + + InputStream input = socket.getInputStream(); + consumeRequest(input); + + OutputStream output = socket.getOutputStream(); + String serverResponse = "" + + "HTTP/1.1 200 OK\r\n" + + "Connection: close\r\n"; + if (chunked) + { + serverResponse += "" + + "Transfer-Encoding: chunked\r\n" + + "\r\n"; + for (int i = 0; i < 2; ++i) + { + serverResponse += + Integer.toHexString(content.length()) + "\r\n" + + content + "\r\n"; + } + serverResponse += "" + + "0\r\n" + + "\r\n"; + } + else + { + serverResponse += "Content-Length: " + content.length() + "\r\n"; + serverResponse += "\r\n"; + serverResponse += content; + } + + output.write(serverResponse.getBytes("UTF-8")); + output.flush(); + if (shutdownOutput) + socket.shutdownOutput(); + + Assert.assertEquals(HttpExchange.STATUS_COMPLETED, exchange.waitForDone()); + Assert.assertEquals(HttpStatus.OK_200, exchange.getResponseStatus()); + + // Give some time to process the connection. + Thread.sleep(1000); + + // Connection should have been removed from pool. + HttpDestination destination = client.getDestination(new Address("localhost", port), false); + Assert.assertEquals(0, destination.getConnections()); + Assert.assertEquals(0, destination.getIdleConnections()); + } + + private boolean consumeRequest(InputStream input) throws IOException + { + int crlfs = 0; + while (true) + { + int read = input.read(); + if (read < 0) + return true; + if (read == '\r' || read == '\n') + ++crlfs; + else + crlfs = 0; + if (crlfs == 4) + return false; + } + } +} diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java new file mode 100644 index 0000000000..9412f8c0d2 --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java @@ -0,0 +1,207 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client; + +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.toolchain.test.TestTracker; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.After; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.Arrays; +import java.util.Collection; + +@RunWith(Parameterized.class) +public class TLSServerConnectionCloseTest +{ + @Parameterized.Parameters(name = "CloseMode: {0}") + public static Collection<Object[]> parameters() + { + return Arrays.asList(new Object[]{CloseMode.NONE}, new Object[]{CloseMode.CLOSE}, new Object[]{CloseMode.ABRUPT}); + } + + @Rule + public final TestTracker tracker = new TestTracker(); + private HttpClient client; + private final CloseMode closeMode; + + public TLSServerConnectionCloseTest(CloseMode closeMode) + { + this.closeMode = closeMode; + } + + private void startClient() throws Exception + { + SslContextFactory sslContextFactory = new SslContextFactory(); + File keystore = MavenTestingUtils.getTestResourceFile("keystore"); + sslContextFactory.setKeyStorePath(keystore.getAbsolutePath()); + sslContextFactory.setKeyStorePassword("storepwd"); + sslContextFactory.setKeyManagerPassword("keypwd"); + + QueuedThreadPool clientThreads = new QueuedThreadPool(); + clientThreads.setName("client"); + client = new HttpClient(sslContextFactory); + client.setThreadPool(clientThreads); + client.start(); + } + + @After + public void dispose() throws Exception + { + if (client != null) + client.stop(); + } + + @Test + public void testServerSendsConnectionCloseWithoutContent() throws Exception + { + testServerSendsConnectionClose(false, ""); + } + + @Test + public void testServerSendsConnectionCloseWithContent() throws Exception + { + testServerSendsConnectionClose(false, "data"); + } + + @Test + public void testServerSendsConnectionCloseWithChunkedContent() throws Exception + { + testServerSendsConnectionClose(true, "data"); + } + + private void testServerSendsConnectionClose(boolean chunked, String content) throws Exception + { + ServerSocket server = new ServerSocket(0); + int port = server.getLocalPort(); + + startClient(); + + ContentExchange exchange = new ContentExchange(true); + exchange.setURL("https://localhost:" + port + "/ctx/path"); + client.send(exchange); + + Socket socket = server.accept(); + SSLContext sslContext = client.getSslContextFactory().getSslContext(); + SSLSocket sslSocket = (SSLSocket)sslContext.getSocketFactory().createSocket(socket, "localhost", port, false); + sslSocket.setUseClientMode(false); + sslSocket.startHandshake(); + + InputStream input = sslSocket.getInputStream(); + consumeRequest(input); + + OutputStream output = sslSocket.getOutputStream(); + String serverResponse = "" + + "HTTP/1.1 200 OK\r\n" + + "Connection: close\r\n"; + if (chunked) + { + serverResponse += "" + + "Transfer-Encoding: chunked\r\n" + + "\r\n"; + for (int i = 0; i < 2; ++i) + { + serverResponse += + Integer.toHexString(content.length()) + "\r\n" + + content + "\r\n"; + } + serverResponse += "" + + "0\r\n" + + "\r\n"; + } + else + { + serverResponse += "Content-Length: " + content.length() + "\r\n"; + serverResponse += "\r\n"; + serverResponse += content; + } + + output.write(serverResponse.getBytes("UTF-8")); + output.flush(); + + switch (closeMode) + { + case NONE: + { + break; + } + case CLOSE: + { + sslSocket.close(); + break; + } + case ABRUPT: + { + socket.shutdownOutput(); + break; + } + default: + { + throw new IllegalStateException(); + } + } + + Assert.assertEquals(HttpExchange.STATUS_COMPLETED, exchange.waitForDone()); + Assert.assertEquals(HttpStatus.OK_200, exchange.getResponseStatus()); + + // Give some time to process the connection. + Thread.sleep(1000); + + // Connection should have been removed from pool. + HttpDestination destination = client.getDestination(new Address("localhost", port), false); + Assert.assertEquals(0, destination.getConnections()); + Assert.assertEquals(0, destination.getIdleConnections()); + } + + private boolean consumeRequest(InputStream input) throws IOException + { + int crlfs = 0; + while (true) + { + int read = input.read(); + if (read < 0) + return true; + if (read == '\r' || read == '\n') + ++crlfs; + else + crlfs = 0; + if (crlfs == 4) + return false; + } + } + + private enum CloseMode + { + NONE, CLOSE, ABRUPT + } +} diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java index baadc63f02..c7bf92d944 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java @@ -18,21 +18,15 @@ package org.eclipse.jetty.http; -import java.io.IOException; - -import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.*; import org.eclipse.jetty.io.BufferCache.CachedBuffer; -import org.eclipse.jetty.io.BufferUtil; -import org.eclipse.jetty.io.Buffers; -import org.eclipse.jetty.io.ByteArrayBuffer; -import org.eclipse.jetty.io.EndPoint; -import org.eclipse.jetty.io.EofException; -import org.eclipse.jetty.io.View; import org.eclipse.jetty.io.bio.StreamEndPoint; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import java.io.IOException; + public class HttpParser implements Parser { private static final Logger LOG = Log.getLogger(HttpParser.class); @@ -172,6 +166,8 @@ public class HttpParser implements Parser /* ------------------------------------------------------------ */ public boolean isComplete() { + if (_responseStatus > 0) + return isState(STATE_END) || isState(STATE_SEEKING_EOF); return isState(STATE_END); } @@ -604,7 +600,7 @@ public class HttpParser implements Parser if (ch == HttpTokens.CARRIAGE_RETURN || ch == HttpTokens.LINE_FEED) { // is it a response that cannot have a body? - if (_responseStatus > 0 && // response + if (_responseStatus > 0 && // response (_responseStatus == 304 || // not-modified response _responseStatus == 204 || // no-content response _responseStatus < 200)) // 1xx response @@ -960,14 +956,14 @@ public class HttpParser implements Parser } case STATE_SEEKING_EOF: - { + { // Close if there is more data than CRLF if (_buffer.length()>2) { _state=STATE_END; _endp.close(); } - else + else { // or if the data is not white space while (_buffer.length()>0) @@ -978,7 +974,7 @@ public class HttpParser implements Parser _buffer.clear(); } } - + _buffer.clear(); break; } |