Fixes #307898 (websocket does not handle messages larger than a buffer).
git-svn-id: svn+ssh://dev.eclipse.org/svnroot/rt/org.eclipse.jetty/jetty/trunk@1436 7e9141cc-0065-0410-87d8-b60c137991c4
This commit is contained in:
parent
012b1e7c22
commit
6fdd5c9677
|
@ -1,6 +1,7 @@
|
||||||
package org.eclipse.jetty.websocket;
|
package org.eclipse.jetty.websocket;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
import org.eclipse.jetty.io.Buffer;
|
import org.eclipse.jetty.io.Buffer;
|
||||||
import org.eclipse.jetty.io.EndPoint;
|
import org.eclipse.jetty.io.EndPoint;
|
||||||
|
@ -25,124 +26,84 @@ public class WebSocketGenerator
|
||||||
_endp=endp;
|
_endp=endp;
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized public void addFrame(byte frame,byte[] content, int blockFor) throws IOException
|
public synchronized void addFrame(byte frame,byte[] content, int blockFor) throws IOException
|
||||||
{
|
{
|
||||||
addFrame(frame,content,0,content.length,blockFor);
|
addFrame(frame,content,0,content.length,blockFor);
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized public void addFrame(byte frame,byte[] content, int offset, int length, int blockFor) throws IOException
|
public synchronized void addFrame(byte frame,byte[] content, int offset, int length, int blockFor) throws IOException
|
||||||
{
|
{
|
||||||
if (_buffer==null)
|
if (_buffer==null)
|
||||||
_buffer=_buffers.getDirectBuffer();
|
_buffer=_buffers.getDirectBuffer();
|
||||||
|
|
||||||
if ((frame&0x80)==0x80)
|
if (_buffer.space() == 0)
|
||||||
{
|
expelBuffer(blockFor);
|
||||||
// Send in a length delimited frame
|
|
||||||
|
|
||||||
// maximum of 3 byte length == 21 bits
|
if ((frame & WebSocket.LENGTH_FRAME) == WebSocket.LENGTH_FRAME)
|
||||||
if (length>2097152)
|
{
|
||||||
throw new IllegalArgumentException("too big");
|
// Send a length delimited frame
|
||||||
int length_bytes=(length>16384)?3:(length>128)?2:1;
|
|
||||||
int needed=length+1+length_bytes;
|
|
||||||
checkSpace(needed,blockFor);
|
|
||||||
|
|
||||||
_buffer.put(frame);
|
_buffer.put(frame);
|
||||||
|
if (_buffer.space() == 0)
|
||||||
|
expelBuffer(blockFor);
|
||||||
|
|
||||||
switch (length_bytes)
|
// How many bytes we need for the length ?
|
||||||
|
// We have 7 bits available, so log2(length) / 7 + 1
|
||||||
|
// For example, 50000 bytes is 2 8-bytes: 11000011 01010000
|
||||||
|
// but we need to write it in 3 7-bytes 0000011 0000110 1010000
|
||||||
|
// 65536 == 1 00000000 00000000 => 100 0000000 0000000
|
||||||
|
int lengthBytes = new BigInteger(String.valueOf(length)).bitLength() / 7 + 1;
|
||||||
|
for (int i = lengthBytes - 1; i >= 0; --i)
|
||||||
{
|
{
|
||||||
case 3:
|
byte lengthByte = (byte)(0x80 | (0x7F & (length >> 7 * i)));
|
||||||
_buffer.put((byte)(0x80|(length>>14)));
|
_buffer.put(lengthByte);
|
||||||
case 2:
|
if (_buffer.space() == 0)
|
||||||
_buffer.put((byte)(0x80|(0x7f&(length>>7))));
|
expelBuffer(blockFor);
|
||||||
case 1:
|
|
||||||
_buffer.put((byte)(0x7f&length));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_buffer.put(content,offset,length);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// send in a sentinel frame
|
|
||||||
int needed=length+2;
|
|
||||||
checkSpace(needed,blockFor);
|
|
||||||
|
|
||||||
_buffer.put(frame);
|
_buffer.put(frame);
|
||||||
_buffer.put(content,offset,length);
|
}
|
||||||
|
|
||||||
|
if (_buffer.space() == 0)
|
||||||
|
expelBuffer(blockFor);
|
||||||
|
|
||||||
|
int remaining = length;
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
int chunk = remaining < _buffer.space() ? remaining : _buffer.space();
|
||||||
|
_buffer.put(content, offset + (length - remaining), chunk);
|
||||||
|
remaining -= chunk;
|
||||||
|
if (_buffer.space() > 0)
|
||||||
|
{
|
||||||
|
if (frame == WebSocket.SENTINEL_FRAME)
|
||||||
_buffer.put((byte)0xFF);
|
_buffer.put((byte)0xFF);
|
||||||
}
|
// Gently flush the data, issuing a non-blocking write
|
||||||
}
|
flushBuffer();
|
||||||
|
|
||||||
synchronized public void addFrame(byte frame, String content, int blockFor) throws IOException
|
|
||||||
{
|
|
||||||
Buffer byte_buffer=_buffers.getBuffer();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
byte[] array=byte_buffer.array();
|
|
||||||
|
|
||||||
int chars = content.length();
|
|
||||||
int bytes = 0;
|
|
||||||
final int limit=array.length-6;
|
|
||||||
|
|
||||||
for (int i = 0; i < chars; i++)
|
|
||||||
{
|
|
||||||
int code = content.charAt(i);
|
|
||||||
|
|
||||||
if (bytes>=limit)
|
|
||||||
throw new IllegalArgumentException("frame too large");
|
|
||||||
|
|
||||||
if ((code & 0xffffff80) == 0)
|
|
||||||
{
|
|
||||||
array[bytes++]=(byte)(code);
|
|
||||||
}
|
|
||||||
else if((code&0xfffff800)==0)
|
|
||||||
{
|
|
||||||
array[bytes++]=(byte)(0xc0|(code>>6));
|
|
||||||
array[bytes++]=(byte)(0x80|(code&0x3f));
|
|
||||||
}
|
|
||||||
else if((code&0xffff0000)==0)
|
|
||||||
{
|
|
||||||
array[bytes++]=(byte)(0xe0|(code>>12));
|
|
||||||
array[bytes++]=(byte)(0x80|((code>>6)&0x3f));
|
|
||||||
array[bytes++]=(byte)(0x80|(code&0x3f));
|
|
||||||
}
|
|
||||||
else if((code&0xff200000)==0)
|
|
||||||
{
|
|
||||||
array[bytes++]=(byte)(0xf0|(code>>18));
|
|
||||||
array[bytes++]=(byte)(0x80|((code>>12)&0x3f));
|
|
||||||
array[bytes++]=(byte)(0x80|((code>>6)&0x3f));
|
|
||||||
array[bytes++]=(byte)(0x80|(code&0x3f));
|
|
||||||
}
|
|
||||||
else if((code&0xf4000000)==0)
|
|
||||||
{
|
|
||||||
array[bytes++]=(byte)(0xf8|(code>>24));
|
|
||||||
array[bytes++]=(byte)(0x80|((code>>18)&0x3f));
|
|
||||||
array[bytes++]=(byte)(0x80|((code>>12)&0x3f));
|
|
||||||
array[bytes++]=(byte)(0x80|((code>>6)&0x3f));
|
|
||||||
array[bytes++]=(byte)(0x80|(code&0x3f));
|
|
||||||
}
|
|
||||||
else if((code&0x80000000)==0)
|
|
||||||
{
|
|
||||||
array[bytes++]=(byte)(0xfc|(code>>30));
|
|
||||||
array[bytes++]=(byte)(0x80|((code>>24)&0x3f));
|
|
||||||
array[bytes++]=(byte)(0x80|((code>>18)&0x3f));
|
|
||||||
array[bytes++]=(byte)(0x80|((code>>12)&0x3f));
|
|
||||||
array[bytes++]=(byte)(0x80|((code>>6)&0x3f));
|
|
||||||
array[bytes++]=(byte)(0x80|(code&0x3f));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
array[bytes++]=(byte)('?');
|
// Forcibly flush the data, issuing a blocking write
|
||||||
}
|
expelBuffer(blockFor);
|
||||||
}
|
if (remaining == 0)
|
||||||
addFrame(frame,array,0,bytes,blockFor);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
{
|
||||||
_buffers.returnBuffer(byte_buffer);
|
if (frame == WebSocket.SENTINEL_FRAME)
|
||||||
|
_buffer.put((byte)0xFF);
|
||||||
|
// Gently flush the data, issuing a non-blocking write
|
||||||
|
flushBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkSpace(int needed, long blockFor)
|
public synchronized void addFrame(byte frame, String content, int blockFor) throws IOException
|
||||||
|
{
|
||||||
|
byte[] bytes = content.getBytes("UTF-8");
|
||||||
|
addFrame(frame, bytes, 0, bytes.length, blockFor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void checkSpace(int needed, long blockFor)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
int space=_buffer.space();
|
int space=_buffer.space();
|
||||||
|
@ -184,12 +145,12 @@ public class WebSocketGenerator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized public int flush(long blockFor)
|
public synchronized int flush(long blockFor)
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized public int flush() throws IOException
|
public synchronized int flush() throws IOException
|
||||||
{
|
{
|
||||||
int flushed = flushBuffer();
|
int flushed = flushBuffer();
|
||||||
if (_buffer!=null && _buffer.length()==0)
|
if (_buffer!=null && _buffer.length()==0)
|
||||||
|
@ -200,10 +161,10 @@ public class WebSocketGenerator
|
||||||
return flushed;
|
return flushed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int flushBuffer() throws IOException
|
private synchronized int flushBuffer() throws IOException
|
||||||
{
|
{
|
||||||
if (!_endp.isOpen())
|
if (!_endp.isOpen())
|
||||||
return -1;
|
throw new IOException("Closed");
|
||||||
|
|
||||||
if (_buffer!=null)
|
if (_buffer!=null)
|
||||||
{
|
{
|
||||||
|
@ -215,7 +176,27 @@ public class WebSocketGenerator
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized public boolean isBufferEmpty()
|
private synchronized void expelBuffer(long blockFor) throws IOException
|
||||||
|
{
|
||||||
|
flushBuffer();
|
||||||
|
_buffer.compact();
|
||||||
|
if (!_endp.isBlocking())
|
||||||
|
{
|
||||||
|
while (_buffer.space()==0)
|
||||||
|
{
|
||||||
|
// TODO: in case the I/O system signals write ready, but when we attempt to write we cannot
|
||||||
|
// TODO: we should decrease the blockFor timeout instead of waiting again the whole timeout
|
||||||
|
boolean ready = _endp.blockWritable(blockFor);
|
||||||
|
if (!ready)
|
||||||
|
throw new IOException("Write timeout");
|
||||||
|
|
||||||
|
flushBuffer();
|
||||||
|
_buffer.compact();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isBufferEmpty()
|
||||||
{
|
{
|
||||||
return _buffer==null || _buffer.length()==0;
|
return _buffer==null || _buffer.length()==0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,183 @@
|
||||||
|
package org.eclipse.jetty.websocket;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
import junit.framework.TestCase;
|
||||||
|
import org.eclipse.jetty.server.Connector;
|
||||||
|
import org.eclipse.jetty.server.Server;
|
||||||
|
import org.eclipse.jetty.server.handler.DefaultHandler;
|
||||||
|
import org.eclipse.jetty.server.nio.SelectChannelConnector;
|
||||||
|
import org.eclipse.jetty.util.StringUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @version $Revision$ $Date$
|
||||||
|
*/
|
||||||
|
public class WebSocketMessageTest extends TestCase
|
||||||
|
{
|
||||||
|
private Server _server;
|
||||||
|
private Connector _connector;
|
||||||
|
private TestWebSocket _serverWebSocket;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setUp() throws Exception
|
||||||
|
{
|
||||||
|
_server = new Server();
|
||||||
|
_connector = new SelectChannelConnector();
|
||||||
|
_server.addConnector(_connector);
|
||||||
|
WebSocketHandler wsHandler = new WebSocketHandler()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
protected WebSocket doWebSocketConnect(HttpServletRequest request, String protocol)
|
||||||
|
{
|
||||||
|
return _serverWebSocket = new TestWebSocket();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
wsHandler.setHandler(new DefaultHandler());
|
||||||
|
_server.setHandler(wsHandler);
|
||||||
|
_server.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void tearDown() throws Exception
|
||||||
|
{
|
||||||
|
_server.stop();
|
||||||
|
_server.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testServerSendBigStringMessage() throws Exception
|
||||||
|
{
|
||||||
|
Socket socket = new Socket("localhost", _connector.getLocalPort());
|
||||||
|
OutputStream output = socket.getOutputStream();
|
||||||
|
output.write(
|
||||||
|
("GET /test HTTP/1.1\r\n" +
|
||||||
|
"Host: localhost\r\n" +
|
||||||
|
"Upgrade: WebSocket\r\n" +
|
||||||
|
"Connection: Upgrade\r\n" +
|
||||||
|
"\r\n").getBytes("ISO-8859-1"));
|
||||||
|
output.flush();
|
||||||
|
|
||||||
|
InputStream input = socket.getInputStream();
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(input, "ISO-8859-1"));
|
||||||
|
String responseLine = reader.readLine();
|
||||||
|
assertTrue(responseLine.startsWith("HTTP/1.1 101 Web Socket Protocol Handshake"));
|
||||||
|
// Read until we find an empty line, which signals the end of the http response
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null)
|
||||||
|
if (line.length() == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
assertTrue(_serverWebSocket.awaitConnected(1000));
|
||||||
|
assertNotNull(_serverWebSocket.outbound);
|
||||||
|
|
||||||
|
// Server sends a big message
|
||||||
|
StringBuilder message = new StringBuilder();
|
||||||
|
String text = "0123456789ABCDEF";
|
||||||
|
for (int i = 0; i < 64 * 1024 / text.length(); ++i)
|
||||||
|
message.append(text);
|
||||||
|
_serverWebSocket.outbound.sendMessage(message.toString());
|
||||||
|
|
||||||
|
// Read until we get 0xFF
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int read = input.read();
|
||||||
|
baos.write(read);
|
||||||
|
if (read == 0xFF)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
baos.close();
|
||||||
|
byte[] bytes = baos.toByteArray();
|
||||||
|
String result = StringUtil.printable(bytes);
|
||||||
|
assertTrue(result.startsWith("0x00"));
|
||||||
|
assertTrue(result.endsWith("0xFF"));
|
||||||
|
assertEquals(message.length() + "0x00".length() + "0xFF".length(), result.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testServerSendBigBinaryMessage() throws Exception
|
||||||
|
{
|
||||||
|
Socket socket = new Socket("localhost", _connector.getLocalPort());
|
||||||
|
OutputStream output = socket.getOutputStream();
|
||||||
|
output.write(
|
||||||
|
("GET /test HTTP/1.1\r\n" +
|
||||||
|
"Host: localhost\r\n" +
|
||||||
|
"Upgrade: WebSocket\r\n" +
|
||||||
|
"Connection: Upgrade\r\n" +
|
||||||
|
"\r\n").getBytes("ISO-8859-1"));
|
||||||
|
output.flush();
|
||||||
|
|
||||||
|
InputStream input = socket.getInputStream();
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(input, "ISO-8859-1"));
|
||||||
|
String responseLine = reader.readLine();
|
||||||
|
assertTrue(responseLine.startsWith("HTTP/1.1 101 Web Socket Protocol Handshake"));
|
||||||
|
// Read until we find an empty line, which signals the end of the http response
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null)
|
||||||
|
if (line.length() == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
assertTrue(_serverWebSocket.awaitConnected(1000));
|
||||||
|
assertNotNull(_serverWebSocket.outbound);
|
||||||
|
|
||||||
|
// Server sends a big message
|
||||||
|
StringBuilder message = new StringBuilder();
|
||||||
|
String text = "0123456789ABCDEF";
|
||||||
|
for (int i = 0; i < 64 * 1024 / text.length(); ++i)
|
||||||
|
message.append(text);
|
||||||
|
byte[] data = message.toString().getBytes("UTF-8");
|
||||||
|
_serverWebSocket.outbound.sendMessage(WebSocket.LENGTH_FRAME, data);
|
||||||
|
|
||||||
|
// I know the format of the message will be: 0x80 0x84 0x80 0x80 ...
|
||||||
|
int frame = input.read();
|
||||||
|
assertEquals(0x80, frame);
|
||||||
|
int length1 = input.read();
|
||||||
|
assertEquals(0x84, length1);
|
||||||
|
int length2 = input.read();
|
||||||
|
assertEquals(0x80, length2);
|
||||||
|
int length3 = input.read();
|
||||||
|
assertEquals(0x80, length3);
|
||||||
|
int read = 0;
|
||||||
|
while (read < data.length)
|
||||||
|
{
|
||||||
|
int b = input.read();
|
||||||
|
assertTrue(b != -1);
|
||||||
|
++read;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestWebSocket implements WebSocket
|
||||||
|
{
|
||||||
|
private final CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
private volatile Outbound outbound;
|
||||||
|
|
||||||
|
public void onConnect(Outbound outbound)
|
||||||
|
{
|
||||||
|
this.outbound = outbound;
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean awaitConnected(long time) throws InterruptedException
|
||||||
|
{
|
||||||
|
return latch.await(time, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onMessage(byte frame, String data)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onMessage(byte frame, byte[] data, int offset, int length)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onDisconnect()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue