393075 Jetty WebSocket client cannot connect to Tomcat WebSocket Server

* Adding testcase to repliate behavior as reported by bug.
This commit is contained in:
Joakim Erdfelt 2012-10-29 11:18:01 -07:00
parent 8b90057b68
commit 6108d0df0c
5 changed files with 441 additions and 5 deletions

View File

@ -28,6 +28,7 @@ import java.util.Map;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.net.ssl.SSLEngine;
import org.eclipse.jetty.http.HttpFields;
@ -389,7 +390,7 @@ public class WebSocketClientFactory extends AggregateLifeCycle
_accept = value.toString();
}
@Override
@Override // TODO simone says shouldn't be needed
public void startRequest(Buffer method, Buffer url, Buffer version) throws IOException
{
if (_error == null)
@ -397,7 +398,7 @@ public class WebSocketClientFactory extends AggregateLifeCycle
_endp.close();
}
@Override
@Override // TODO simone says shouldn't be needed
public void content(Buffer ref) throws IOException
{
if (_error == null)
@ -515,6 +516,7 @@ public class WebSocketClientFactory extends AggregateLifeCycle
private WebSocketConnection newWebSocketConnection() throws IOException
{
__log.debug("newWebSocketConnection()");
return new WebSocketClientConnection(
_future._client.getFactory(),
_future.getWebSocket(),

View File

@ -0,0 +1,122 @@
//
// ========================================================================
// Copyright (c) 1995-2012 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.websocket;
import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.websocket.dummy.DummyServer;
import org.eclipse.jetty.websocket.dummy.DummyServer.ServerConnection;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
public class TomcatServerQuirksTest
{
/**
* Test for when encountering a "Transfer-Encoding: chunked" on a Upgrade Response header.
* <ul>
* <li><a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=393075">Eclipse Jetty Bug #393075</a></li>
* <li><a href="https://issues.apache.org/bugzilla/show_bug.cgi?id=54067">Apache Tomcat Bug #54067</a></li>
* </ul>
* @throws IOException
*/
@Test
@Ignore("Bug with Transfer-Encoding")
public void testTomcat7_0_32_WithTransferEncoding() throws Exception {
DummyServer server = new DummyServer();
int bufferSize = 512;
QueuedThreadPool threadPool = new QueuedThreadPool();
WebSocketClientFactory factory = new WebSocketClientFactory(threadPool, new ZeroMaskGen(), bufferSize);
try {
server.start();
// Setup Client Factory
threadPool.start();
factory.start();
// Create Client
WebSocketClient client = new WebSocketClient(factory);
// Create End User WebSocket Class
final CountDownLatch openLatch = new CountDownLatch(1);
final CountDownLatch dataLatch = new CountDownLatch(1);
WebSocket.OnTextMessage websocket = new WebSocket.OnTextMessage()
{
public void onOpen(Connection connection)
{
openLatch.countDown();
}
public void onMessage(String data)
{
// System.out.println("data = " + data);
dataLatch.countDown();
}
public void onClose(int closeCode, String message)
{
}
};
// Open connection
URI wsURI = server.getWsUri();
client.open(wsURI, websocket);
// Accept incoming connection
ServerConnection socket = server.accept();
socket.setSoTimeout(2000); // timeout
// Issue upgrade
Map<String,String> extraResponseHeaders = new HashMap<String, String>();
extraResponseHeaders.put("Transfer-Encoding", "chunked"); // !! The problem !!
socket.upgrade(extraResponseHeaders);
// Wait for proper upgrade
Assert.assertTrue("Timed out waiting for Client side WebSocket open event", openLatch.await(1, TimeUnit.SECONDS));
// Have server write frame.
int length = bufferSize / 2;
ByteBuffer serverFrame = ByteBuffer.allocate(bufferSize);
serverFrame.put((byte)(0x80 | 0x01)); // FIN + TEXT
serverFrame.put((byte)0x7E); // No MASK and 2 bytes length
serverFrame.put((byte)(length >> 8)); // first length byte
serverFrame.put((byte)(length & 0xFF)); // second length byte
for (int i = 0; i < length; ++i)
serverFrame.put((byte)'x');
serverFrame.flip();
byte buf[] = serverFrame.array();
socket.write(buf,0,buf.length);
socket.flush();
Assert.assertTrue(dataLatch.await(1000, TimeUnit.SECONDS));
} finally {
factory.stop();
threadPool.stop();
server.stop();
}
}
}

View File

@ -18,6 +18,8 @@
package org.eclipse.jetty.websocket;
import static org.hamcrest.Matchers.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
@ -47,9 +49,6 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
public class WebSocketClientTest
{
private WebSocketClientFactory _factory = new WebSocketClientFactory();
@ -103,6 +102,7 @@ public class WebSocketClientTest
{
}
};
client.open(new URI("ws://127.0.0.1:" + _serverPort + "/"), websocket);
Socket socket = _server.accept();

View File

@ -0,0 +1,308 @@
//
// ========================================================================
// Copyright (c) 1995-2012 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.websocket.dummy;
import static org.hamcrest.Matchers.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.WebSocketConnectionRFC6455;
import org.junit.Assert;
/**
* Simple ServerSocket server used to test oddball server scenarios encountered in the real world.
*/
public class DummyServer
{
public static class ServerConnection
{
private static final Logger LOG = Log.getLogger(ServerConnection.class);
private final Socket socket;
private InputStream in;
private OutputStream out;
public ServerConnection(Socket socket)
{
this.socket = socket;
}
public int read(ByteBuffer buf) throws IOException
{
int len = 0;
while ((in.available() > 0) && (buf.remaining() > 0))
{
buf.put((byte)in.read());
len++;
}
return len;
}
public void disconnect()
{
LOG.debug("disconnect");
IO.close(in);
IO.close(out);
if (socket != null)
{
try
{
socket.close();
}
catch (IOException ignore)
{
/* ignore */
}
}
}
public InputStream getInputStream() throws IOException
{
if (in == null)
{
in = socket.getInputStream();
}
return in;
}
public OutputStream getOutputStream() throws IOException
{
if (out == null)
{
out = socket.getOutputStream();
}
return out;
}
public void flush() throws IOException
{
LOG.debug("flush()");
getOutputStream().flush();
}
public String readRequest() throws IOException
{
LOG.debug("Reading client request");
StringBuilder request = new StringBuilder();
BufferedReader in = new BufferedReader(new InputStreamReader(getInputStream()));
for (String line = in.readLine(); line != null; line = in.readLine())
{
if (line.length() == 0)
{
break;
}
request.append(line).append("\r\n");
LOG.debug("read line: {}",line);
}
LOG.debug("Client Request:{}{}","\n",request);
return request.toString();
}
public void respond(String rawstr) throws IOException
{
LOG.debug("respond(){}{}","\n",rawstr);
getOutputStream().write(rawstr.getBytes());
flush();
}
public void setSoTimeout(int ms) throws SocketException
{
socket.setSoTimeout(ms);
}
public void upgrade(Map<String, String> extraResponseHeaders) throws IOException
{
@SuppressWarnings("unused")
Pattern patExts = Pattern.compile("^Sec-WebSocket-Extensions: (.*)$",Pattern.CASE_INSENSITIVE);
Pattern patKey = Pattern.compile("^Sec-WebSocket-Key: (.*)$",Pattern.CASE_INSENSITIVE);
LOG.debug("(Upgrade) Reading HTTP Request");
Matcher mat;
String key = "not sent";
BufferedReader in = new BufferedReader(new InputStreamReader(getInputStream()));
for (String line = in.readLine(); line != null; line = in.readLine())
{
if (line.length() == 0)
{
break;
}
// TODO: Check for extensions
// mat = patExts.matcher(line);
// if (mat.matches())
// Check for Key
mat = patKey.matcher(line);
if (mat.matches())
{
key = mat.group(1);
}
}
LOG.debug("(Upgrade) Writing HTTP Response");
// TODO: handle extensions?
// Setup Response
StringBuilder resp = new StringBuilder();
resp.append("HTTP/1.1 101 Upgrade\r\n");
resp.append("Upgrade: websocket\r\n");
resp.append("Connection: upgrade\r\n");
resp.append("Sec-WebSocket-Accept: ");
resp.append(WebSocketConnectionRFC6455.hashKey(key)).append("\r\n");
// extra response headers.
if (extraResponseHeaders != null)
{
for (Map.Entry<String,String> header : extraResponseHeaders.entrySet())
{
resp.append(header.getKey());
resp.append(": ");
resp.append(header.getValue());
resp.append("\r\n");
}
}
resp.append("\r\n");
// Write Response
getOutputStream().write(resp.toString().getBytes());
flush();
}
public void write(byte[] bytes) throws IOException
{
LOG.debug("Writing {} bytes", bytes.length);
getOutputStream().write(bytes);
}
public void write(byte[] buf, int offset, int length) throws IOException
{
LOG.debug("Writing bytes[{}], offset={}, length={}", buf.length, offset, length);
getOutputStream().write(buf,offset,length);
}
public void write(int b) throws IOException
{
LOG.debug("Writing int={}", b);
getOutputStream().write(b);
}
}
private static final Logger LOG = Log.getLogger(DummyServer.class);
private ServerSocket serverSocket;
private URI wsUri;
public ServerConnection accept() throws IOException
{
LOG.debug(".accept()");
assertIsStarted();
Socket socket = serverSocket.accept();
return new ServerConnection(socket);
}
private void assertIsStarted()
{
Assert.assertThat("ServerSocket",serverSocket,notNullValue());
Assert.assertThat("ServerSocket.isBound",serverSocket.isBound(),is(true));
Assert.assertThat("ServerSocket.isClosed",serverSocket.isClosed(),is(false));
Assert.assertThat("WsUri",wsUri,notNullValue());
}
public URI getWsUri()
{
return wsUri;
}
public void respondToClient(Socket connection, String serverResponse) throws IOException
{
InputStream in = null;
InputStreamReader isr = null;
BufferedReader buf = null;
OutputStream out = null;
try
{
in = connection.getInputStream();
isr = new InputStreamReader(in);
buf = new BufferedReader(isr);
String line;
while ((line = buf.readLine()) != null)
{
// System.err.println(line);
if (line.length() == 0)
{
// Got the "\r\n" line.
break;
}
}
// System.out.println("[Server-Out] " + serverResponse);
out = connection.getOutputStream();
out.write(serverResponse.getBytes());
out.flush();
}
finally
{
IO.close(buf);
IO.close(isr);
IO.close(in);
IO.close(out);
}
}
public void start() throws IOException
{
serverSocket = new ServerSocket();
InetAddress addr = InetAddress.getByName("localhost");
InetSocketAddress endpoint = new InetSocketAddress(addr,0);
serverSocket.bind(endpoint);
int port = serverSocket.getLocalPort();
String uri = String.format("ws://%s:%d/",addr.getHostAddress(),port);
wsUri = URI.create(uri);
LOG.debug("Server Started on {} -> {}",endpoint,wsUri);
}
public void stop()
{
LOG.debug("Stopping Server");
try
{
serverSocket.close();
}
catch (IOException ignore)
{
/* ignore */
}
}
}

View File

@ -0,0 +1,4 @@
# Setup default logging implementation for during testing
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
org.eclipse.jetty.LEVEL=INFO
org.eclipse.jetty.websocket.LEVEL=DEBUG