Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimone Bordet2015-09-21 13:17:36 +0000
committerSimone Bordet2015-09-21 13:54:25 +0000
commit144da724c991689f00e80b36fde8d28fdffd4e6c (patch)
tree679262162accf66b74dbe79fef233c0d40a0abdf
parent01ef307198c4feb0e842c9ce067c1f1669412b45 (diff)
downloadorg.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.
-rw-r--r--jetty-client/src/test/java/org/eclipse/jetty/client/ServerConnectionCloseTest.java168
-rw-r--r--jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java207
-rw-r--r--jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java22
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;
}

Back to the top