Author: markt Date: Sun Jun 3 18:50:48 2012 New Revision: 1345739 URL: http://svn.apache.org/viewvc?rev=1345739&view=rev Log: Fix https://issues.apache.org/bugzilla/show_bug.cgi?id=53339 Ensure WebSocket event calls are made using the web application's class loader. Includes a test case.
Modified: tomcat/tc7.0.x/trunk/ (props changed) tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/StreamInbound.java tomcat/tc7.0.x/trunk/test/org/apache/catalina/websocket/TestWebSocket.java tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml Propchange: tomcat/tc7.0.x/trunk/ ------------------------------------------------------------------------------ Merged /tomcat/trunk:r1345737 Modified: tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/StreamInbound.java URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/StreamInbound.java?rev=1345739&r1=1345738&r2=1345739&view=diff ============================================================================== --- tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/StreamInbound.java (original) +++ tomcat/tc7.0.x/trunk/java/org/apache/catalina/websocket/StreamInbound.java Sun Jun 3 18:50:48 2012 @@ -37,12 +37,17 @@ import org.apache.tomcat.util.net.Abstra */ public abstract class StreamInbound implements UpgradeInbound { + private final ClassLoader applicationClassLoader; private UpgradeProcessor<?> processor = null; private WsOutbound outbound; private int outboundByteBufferSize = WsOutbound.DEFAULT_BUFFER_SIZE; private int outboundCharBufferSize = WsOutbound.DEFAULT_BUFFER_SIZE; + public StreamInbound() { + applicationClassLoader = Thread.currentThread().getContextClassLoader(); + } + public int getOutboundByteBufferSize() { return outboundByteBufferSize; @@ -122,11 +127,11 @@ public abstract class StreamInbound impl byte opCode = frame.getOpCode(); if (opCode == Constants.OPCODE_BINARY) { - onBinaryData(wsIs); + doOnBinaryData(wsIs); } else if (opCode == Constants.OPCODE_TEXT) { InputStreamReader r = new InputStreamReader(wsIs, new Utf8Decoder()); - onTextData(r); + doOnTextData(r); } else if (opCode == Constants.OPCODE_CLOSE){ closeOutboundConnection(frame); return SocketState.CLOSED; @@ -159,11 +164,37 @@ public abstract class StreamInbound impl return SocketState.UPGRADED; } + private void doOnBinaryData(InputStream is) throws IOException { + // Need to call onClose using the web application's class loader + Thread t = Thread.currentThread(); + ClassLoader cl = t.getContextClassLoader(); + t.setContextClassLoader(applicationClassLoader); + try { + onBinaryData(is); + } finally { + t.setContextClassLoader(cl); + } + } + + + private void doOnTextData(Reader r) throws IOException { + // Need to call onClose using the web application's class loader + Thread t = Thread.currentThread(); + ClassLoader cl = t.getContextClassLoader(); + t.setContextClassLoader(applicationClassLoader); + try { + onTextData(r); + } finally { + t.setContextClassLoader(cl); + } + } + + private void closeOutboundConnection(int status, ByteBuffer data) throws IOException { try { getWsOutbound().close(status, data); } finally { - onClose(status); + doOnClose(status); } } @@ -171,13 +202,33 @@ public abstract class StreamInbound impl try { getWsOutbound().close(frame); } finally { - onClose(Constants.OPCODE_CLOSE); + doOnClose(Constants.OPCODE_CLOSE); + } + } + + private void doOnClose(int status) { + // Need to call onClose using the web application's class loader + Thread t = Thread.currentThread(); + ClassLoader cl = t.getContextClassLoader(); + t.setContextClassLoader(applicationClassLoader); + try { + onClose(status); + } finally { + t.setContextClassLoader(cl); } } @Override - public void onUpgradeComplete() { - onOpen(outbound); + public final void onUpgradeComplete() { + // Need to call onOpen using the web application's class loader + Thread t = Thread.currentThread(); + ClassLoader cl = t.getContextClassLoader(); + t.setContextClassLoader(applicationClassLoader); + try { + onOpen(outbound); + } finally { + t.setContextClassLoader(cl); + } } /** Modified: tomcat/tc7.0.x/trunk/test/org/apache/catalina/websocket/TestWebSocket.java URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/test/org/apache/catalina/websocket/TestWebSocket.java?rev=1345739&r1=1345738&r2=1345739&view=diff ============================================================================== --- tomcat/tc7.0.x/trunk/test/org/apache/catalina/websocket/TestWebSocket.java (original) +++ tomcat/tc7.0.x/trunk/test/org/apache/catalina/websocket/TestWebSocket.java Sun Jun 3 18:50:48 2012 @@ -28,16 +28,23 @@ import java.io.Writer; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; import java.security.MessageDigest; import java.util.ArrayList; import java.util.List; +import javax.naming.InitialContext; +import javax.naming.NamingException; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.junit.Test; +import org.apache.catalina.Context; +import org.apache.catalina.deploy.ContextEnvironment; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.TomcatBaseTest; import org.apache.catalina.util.Base64; @@ -117,7 +124,6 @@ public class TestWebSocket extends Tomca String responseLine = client.reader.readLine(); assertTrue(responseLine.startsWith("HTTP/1.1 426")); - // Swallow the headers List<String> headerlines = new ArrayList<String>(); String responseHeaderLine = client.reader.readLine(); @@ -208,9 +214,7 @@ public class TestWebSocket extends Tomca String responseLine = client.reader.readLine(); assertTrue(responseLine.startsWith("HTTP/1.1 101")); - // Swallow the headers String accept = null; - String responseHeaderLine = client.reader.readLine(); while (!responseHeaderLine.equals("")) { if(responseHeaderLine.startsWith("Sec-WebSocket-Accept: ")) { @@ -235,6 +239,105 @@ public class TestWebSocket extends Tomca } + @Test + public void testBug53339() throws Exception { + Tomcat tomcat = getTomcatInstance(); + tomcat.enableNaming(); + + // Must have a real docBase - just use temp + Context ctx = + tomcat.addContext("", System.getProperty("java.io.tmpdir")); + + Tomcat.addServlet(ctx, "Bug53339", new Bug53339Servlet()); + ctx.addServletMapping("/*", "Bug53339"); + + // Create the resource + ContextEnvironment env = new ContextEnvironment(); + env.setName(Bug53339WsInbound.JNDI_NAME); + env.setType(String.class.getName()); + env.setValue(Bug53339WsInbound.TEST_MESSAGE); + ctx.getNamingResources().addEnvironment(env); + + tomcat.start(); + + WebSocketClient client= new WebSocketClient(getPort()); + + // Send the WebSocket handshake + client.writer.write("GET / HTTP/1.1" + CRLF); + client.writer.write("Host: foo" + CRLF); + client.writer.write("Upgrade: websocket" + CRLF); + client.writer.write("Connection: upgrade" + CRLF); + client.writer.write("Sec-WebSocket-Version: 13" + CRLF); + client.writer.write("Sec-WebSocket-Key: TODO" + CRLF); + client.writer.write(CRLF); + client.writer.flush(); + + // Make sure we got an upgrade response + String responseLine = client.reader.readLine(); + assertTrue(responseLine.startsWith("HTTP/1.1 101")); + + // Swallow the headers + String responseHeaderLine = client.reader.readLine(); + while (!responseHeaderLine.equals("")) { + responseHeaderLine = client.reader.readLine(); + } + + // Now we can do WebSocket + String msg = client.readMessage(); + assertEquals(Bug53339WsInbound.TEST_MESSAGE, msg); + + // Finished with the socket + client.close(); + } + + + private static class Bug53339Servlet extends WebSocketServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected StreamInbound createWebSocketInbound(String subProtocol) { + return new Bug53339WsInbound(); + } + } + + + private static class Bug53339WsInbound extends MessageInbound { + + public static final String TEST_MESSAGE = "Test Message"; + public static final String JNDI_NAME = "Bug53339Message"; + + @Override + protected void onOpen(WsOutbound outbound) { + String msg = "Error"; + try { + javax.naming.Context initCtx = new InitialContext(); + msg = (String) initCtx.lookup( + "java:comp/env/" + JNDI_NAME); + } catch (NamingException e) { + // Ignore - the test checks if the message is sent + e.printStackTrace(); // for debug purposes if the test fails + } + CharBuffer cb = CharBuffer.wrap("" + msg); + try { + outbound.writeTextMessage(cb); + } catch (IOException e) { + // Ignore - the test checks if the message is sent + } + + } + + @Override + protected void onBinaryMessage(ByteBuffer message) throws IOException { + // Ignore + } + + @Override + protected void onTextMessage(CharBuffer message) throws IOException { + // Ignore + } + } + private static class WebSocketClient { private OutputStream os; private InputStream is; Modified: tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml?rev=1345739&r1=1345738&r2=1345739&view=diff ============================================================================== --- tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml (original) +++ tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml Sun Jun 3 18:50:48 2012 @@ -190,6 +190,10 @@ by Rossen Stoyanchev. (markt) </fix> <fix> + <bug>53339</bug>: Ensure WebSocket call backs (<code>onOpen</code> etc.) + are called using the web application's class loader. (markt) + </fix> + <fix> <bug>53342</bug>: To avoid BindException, make startStopThreads into a demon thread. (kfujino) </fix> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org