Bug 367099 - Upgrade jetty-websocket for RFC 6455

+ Adding new RFC declared close codes 1011 (CLOSE_SERVER_ERROR) and
  1015 (CLOSE_FAILED_TLS_HANDSHAKE)
+ Adding support for responding as CLOSE_SERVER_ERROR if an unhandled
  exception (similar to how HTTP error 500 works) but for exceptions
  thrown out of implementations of WebSocket.
+ Adding guard to prevent use of CLOSE_FAILED_TLS_HANDSHAKE on
  close control frame.
+ Adding unit test for the CLOSE_SERVER_ERROR case.
+ Adding unit test for HTTP response 400 on bad Sec-WebSocket-Version
  request header value.
This commit is contained in:
Joakim Erdfelt 2011-12-19 12:29:15 -07:00
parent 874a6502f4
commit 95f763fab4
5 changed files with 224 additions and 30 deletions

View File

@ -101,6 +101,8 @@ public class WebSocketConnectionRFC6455 extends AbstractConnection implements We
final static int CLOSE_POLICY_VIOLATION=1008;
final static int CLOSE_MESSAGE_TOO_LARGE=1009;
final static int CLOSE_REQUIRED_EXTENSION=1010;
final static int CLOSE_SERVER_ERROR=1011;
final static int CLOSE_FAILED_TLS_HANDSHAKE=1015;
final static int FLAG_FIN=0x8;
@ -372,13 +374,18 @@ public class WebSocketConnectionRFC6455 extends AbstractConnection implements We
{
if (!closed_out)
{
// Close code 1005/1006 are never to be sent as a status over
// Close code 1005/1006/1015 are never to be sent as a status over
// a Close control frame. Code<-1 also means no node.
if (code<0 || (code == WebSocketConnectionRFC6455.CLOSE_NO_CODE) || code==WebSocketConnectionRFC6455.CLOSE_NO_CLOSE)
code=-1;
else if (code==0)
code=WebSocketConnectionRFC6455.CLOSE_NORMAL;
if (code < 0 || (code == WebSocketConnectionRFC6455.CLOSE_NO_CODE) || (code == WebSocketConnectionRFC6455.CLOSE_NO_CLOSE)
|| (code == WebSocketConnectionRFC6455.CLOSE_FAILED_TLS_HANDSHAKE))
{
code = -1;
}
else if (code == 0)
{
code = WebSocketConnectionRFC6455.CLOSE_NORMAL;
}
byte[] bytes = ("xx"+(message==null?"":message)).getBytes(StringUtil.__ISO_8859_1);
bytes[0]=(byte)(code/0x100);
@ -769,7 +776,7 @@ public class WebSocketConnectionRFC6455 extends AbstractConnection implements We
code == WebSocketConnectionRFC6455.CLOSE_UNDEFINED ||
code == WebSocketConnectionRFC6455.CLOSE_NO_CLOSE ||
code == WebSocketConnectionRFC6455.CLOSE_NO_CODE ||
( code > 1010 && code <= 2999 ) ||
( code > 1011 && code <= 2999 ) ||
code >= 5000 )
{
errorClose(WebSocketConnectionRFC6455.CLOSE_PROTOCOL,"Invalid close code " + code);
@ -874,15 +881,15 @@ public class WebSocketConnectionRFC6455 extends AbstractConnection implements We
}
catch(Utf8Appendable.NotUtf8Exception notUtf8)
{
LOG.warn("{} for {}",notUtf8,_endp);
LOG.warn("NOTUTF8 - {} for {}",notUtf8,_endp, notUtf8);
LOG.debug(notUtf8);
errorClose(WebSocketConnectionRFC6455.CLOSE_BAD_PAYLOAD,"Invalid UTF-8");
}
catch(Throwable probablyNotUtf8)
catch(Throwable e)
{
LOG.warn("{} for {}",probablyNotUtf8,_endp);
LOG.debug(probablyNotUtf8);
errorClose(WebSocketConnectionRFC6455.CLOSE_BAD_PAYLOAD,"Invalid Payload: "+probablyNotUtf8);
LOG.warn("{} for {}",e,_endp, e);
LOG.debug(e);
errorClose(WebSocketConnectionRFC6455.CLOSE_SERVER_ERROR,"Internal Server Error: "+e);
}
}

View File

@ -261,7 +261,7 @@ public class WebSocketFactory
// Per RFC 6455 - 4.4 - Supporting Multiple Versions of WebSocket Protocol
// Using the examples as outlined
response.setHeader("Sec-WebSocket-Version","13, 8, 6, 0");
throw new HttpException(400, "Unsupported draft specification: " + draft);
throw new HttpException(400, "Unsupported websocket version specification: " + draft);
}
// Set the defaults

View File

@ -24,13 +24,12 @@ import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.log.StdErrLog;
import org.eclipse.jetty.websocket.helper.CaptureSocket;
import org.eclipse.jetty.websocket.helper.MessageSender;
import org.eclipse.jetty.websocket.helper.WebSocketCaptureServlet;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
/**
@ -43,14 +42,6 @@ public class WebSocketCommTest
private WebSocketCaptureServlet servlet;
private URI serverUri;
@BeforeClass
public static void initLogging()
{
// Configure Logging
System.setProperty("org.eclipse.jetty.util.log.class",StdErrLog.class.getName());
System.setProperty("org.eclipse.jetty.LEVEL","DEBUG");
}
@Before
public void startServer() throws Exception
{
@ -79,6 +70,19 @@ public class WebSocketCommTest
System.out.printf("Server URI: %s%n",serverUri);
}
@After
public void stopServer()
{
try
{
server.stop();
}
catch (Exception e)
{
e.printStackTrace(System.err);
}
}
@Test
public void testSendTextMessages() throws Exception
{
@ -105,10 +109,11 @@ public class WebSocketCommTest
CaptureSocket socket = servlet.captures.get(0);
Assert.assertThat("CaptureSocket",socket,notNullValue());
Assert.assertThat("CaptureSocket.isConnected", socket.isConnected(), is(true));
Assert.assertThat("CaptureSocket.isConnected",socket.isConnected(),is(true));
// Give servlet 500 millisecond to process messages
threadSleep(500,TimeUnit.MILLISECONDS);
TimeUnit.MILLISECONDS.sleep(500);
// Should have captured 5 messages.
Assert.assertThat("CaptureSocket.messages.size",socket.messages.size(),is(5));
}
@ -118,10 +123,4 @@ public class WebSocketCommTest
sender.close();
}
}
public static void threadSleep(int dur, TimeUnit unit) throws InterruptedException
{
long ms = TimeUnit.MILLISECONDS.convert(dur,unit);
Thread.sleep(ms);
}
}

View File

@ -0,0 +1,174 @@
package org.eclipse.jetty.websocket;
import static org.hamcrest.Matchers.*;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.websocket.helper.MessageSender;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Test various <a href="http://tools.ietf.org/html/rfc6455">RFC 6455</a> specified requirements placed on
* {@link WebSocketServlet}
* <p>
* This test serves a different purpose than than the {@link WebSocketGeneratorRFC6455Test},
* {@link WebSocketMessageRFC6455Test}, and {@link WebSocketParserRFC6455Test} tests.
*/
public class WebSocketServletRFCTest
{
private static class RFCSocket implements WebSocket, WebSocket.OnTextMessage
{
private Connection conn;
public void onOpen(Connection connection)
{
this.conn = connection;
}
public void onClose(int closeCode, String message)
{
this.conn = null;
}
public void onMessage(String data)
{
// Test the RFC 6455 close code 1011 that should close
// trigger a WebSocket server terminated close.
if (data.equals("CRASH"))
{
throw new RuntimeException("Something bad happened");
}
// echo the message back.
try
{
conn.sendMessage(data);
}
catch (IOException e)
{
e.printStackTrace(System.err);
}
}
}
@SuppressWarnings("serial")
private static class RFCServlet extends WebSocketServlet
{
public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol)
{
return new RFCSocket();
}
}
private static Server server;
private static URI serverUri;
@BeforeClass
public static void startServer() throws Exception
{
// Configure Server
server = new Server(0);
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");
server.setHandler(context);
// Serve capture servlet
context.addServlet(new ServletHolder(new RFCServlet()),"/");
// Start Server
server.start();
Connector conn = server.getConnectors()[0];
String host = conn.getHost();
if (host == null)
{
host = "localhost";
}
int port = conn.getLocalPort();
serverUri = new URI(String.format("ws://%s:%d/",host,port));
System.out.printf("Server URI: %s%n",serverUri);
}
@AfterClass
public static void stopServer()
{
try
{
server.stop();
}
catch (Exception e)
{
e.printStackTrace(System.err);
}
}
/**
* Test the requirement of responding with an http 400 when using a Sec-WebSocket-Version that is unsupported.
*/
@Test
public void testResponseOnInvalidVersion() throws Exception
{
// Using straight HttpUrlConnection to accomplish this as jetty's WebSocketClient
// doesn't allow the use of invalid versions. (obviously)
URL url = new URL("http://" + serverUri.getHost() + ":" + serverUri.getPort() + "/");
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.setRequestProperty("Upgrade","WebSocket");
conn.setRequestProperty("Connection","Upgrade");
conn.setRequestProperty("Sec-WebSocket-Version","29");
conn.connect();
Assert.assertEquals("Response Code",400,conn.getResponseCode());
Assert.assertThat("Response Message",conn.getResponseMessage(),containsString("Unsupported websocket version specification"));
Assert.assertThat("Response Header Versions",conn.getHeaderField("Sec-WebSocket-Version"),is("13, 8, 6, 0"));
conn.disconnect();
}
/**
* Test the requirement of responding with server terminated close code 1011 when there is an unhandled (internal
* server error) being produced by the extended WebSocketServlet.
*/
@Test
public void testResponseOnInternalError() throws Exception
{
WebSocketClientFactory clientFactory = new WebSocketClientFactory();
clientFactory.start();
WebSocketClient wsc = clientFactory.newWebSocketClient();
MessageSender sender = new MessageSender();
wsc.open(serverUri,sender);
try
{
sender.awaitConnect();
sender.sendMessage("CRASH");
// Give servlet 500 millisecond to process messages
TimeUnit.MILLISECONDS.sleep(500);
Assert.assertThat("WebSocket should be closed", sender.isConnected(), is(false));
Assert.assertThat("WebSocket close clode", sender.getCloseCode(), is(1011));
}
finally
{
sender.close();
}
}
}

View File

@ -25,6 +25,8 @@ public class MessageSender implements WebSocket
{
private Connection conn;
private CountDownLatch connectLatch = new CountDownLatch(1);
private int closeCode = -1;
private String closeMessage = null;
public void onOpen(Connection connection)
{
@ -35,6 +37,8 @@ public class MessageSender implements WebSocket
public void onClose(int closeCode, String message)
{
this.conn = null;
this.closeCode = closeCode;
this.closeMessage = message;
}
public boolean isConnected()
@ -45,6 +49,16 @@ public class MessageSender implements WebSocket
}
return this.conn.isOpen();
}
public int getCloseCode()
{
return closeCode;
}
public String getCloseMessage()
{
return closeMessage;
}
public void sendMessage(String format, Object... args) throws IOException
{