From 995df89b2225a9a9e0c4aea582c3c08119d7fe2a Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 3 Sep 2010 03:41:05 +0000 Subject: [PATCH] 324369 Implement draft-ietf-hybi-thewebsocketprotocol-01 git-svn-id: svn+ssh://dev.eclipse.org/svnroot/rt/org.eclipse.jetty/jetty/trunk@2242 7e9141cc-0065-0410-87d8-b60c137991c4 --- VERSION.txt | 1 + .../jetty/client/WebSocketUpgradeTest.java | 2 +- .../jetty/websocket/WebSocketConnection.java | 117 +++++++--- .../jetty/websocket/WebSocketFactory.java | 3 +- .../jetty/websocket/WebSocketGenerator.java | 193 +--------------- .../websocket/WebSocketGeneratorD00.java | 168 ++++++++++++++ .../websocket/WebSocketGeneratorD01.java | 181 +++++++++++++++ .../jetty/websocket/WebSocketParser.java | 213 +---------------- .../jetty/websocket/WebSocketParserD00.java | 191 ++++++++++++++++ .../jetty/websocket/WebSocketParserD01.java | 214 ++++++++++++++++++ .../websocket/WebSocketGeneratorD00Test.java | 112 +++++++++ ...st.java => WebSocketGeneratorD01Test.java} | 39 +--- .../jetty/websocket/WebSocketLoadTest.java | 7 +- .../jetty/websocket/WebSocketMessageTest.java | 9 +- ...rTest.java => WebSocketParserD00Test.java} | 28 +-- .../websocket/WebSocketParserD01Test.java | 187 +++++++++++++++ .../jetty/websocket/WebSocketTest.java | 47 +++- .../jetty/websocket/WebSocketTestServer.java | 1 + 18 files changed, 1229 insertions(+), 484 deletions(-) create mode 100644 jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketGeneratorD00.java create mode 100644 jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketGeneratorD01.java create mode 100644 jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketParserD00.java create mode 100644 jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketParserD01.java create mode 100644 jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketGeneratorD00Test.java rename jetty-websocket/src/test/java/org/eclipse/jetty/websocket/{WebSocketGeneratorTest.java => WebSocketGeneratorD01Test.java} (68%) rename jetty-websocket/src/test/java/org/eclipse/jetty/websocket/{WebSocketParserTest.java => WebSocketParserD00Test.java} (87%) create mode 100644 jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketParserD01Test.java diff --git a/VERSION.txt b/VERSION.txt index 8695de29ed6..386597066df 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -23,6 +23,7 @@ jetty-7.2-SNAPSHOT + 324260 Jetty-6 continuations handle complete calls + 324359 illegal actions on AsyncContext should not change its state. + 324360 validate input on getResource since loop logic obscures subclass input validation. + + 324369 Implement draft-ietf-hybi-thewebsocketprotocol-01 + JETTY-912 added per exchange timeout api + JETTY-1245 Do not use direct buffers with NIO SSL + JETTY-1249 Apply max idle time to all connectors diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/WebSocketUpgradeTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/WebSocketUpgradeTest.java index e1bdfc41e23..fd1974edf35 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/WebSocketUpgradeTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/WebSocketUpgradeTest.java @@ -119,7 +119,7 @@ public class WebSocketUpgradeTest extends TestCase protected Connection onSwitchProtocol(EndPoint endp) throws IOException { waitFor(3); - WebSocketConnection connection = new WebSocketConnection(clientWS,endp); + WebSocketConnection connection = new WebSocketConnection(clientWS,endp,0); _results.add("onSwitchProtocol"); _results.add(connection); diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnection.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnection.java index 7fe42017cf2..bb1ff2359d0 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnection.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketConnection.java @@ -15,6 +15,7 @@ import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.nio.IndirectNIOBuffer; import org.eclipse.jetty.io.nio.SelectChannelEndPoint; import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.util.Utf8StringBuilder; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.thread.Timeout; @@ -30,13 +31,13 @@ public class WebSocketConnection implements Connection, WebSocket.Outbound String _key2; ByteArrayBuffer _hixie; - public WebSocketConnection(WebSocket websocket, EndPoint endpoint) + public WebSocketConnection(WebSocket websocket, EndPoint endpoint,int draft) throws IOException { - this(websocket,endpoint,new WebSocketBuffers(8192),System.currentTimeMillis(),300000); + this(websocket,endpoint,new WebSocketBuffers(8192),System.currentTimeMillis(),300000,draft); } - public WebSocketConnection(WebSocket websocket, EndPoint endpoint, WebSocketBuffers buffers, long timestamp, int maxIdleTime) + public WebSocketConnection(WebSocket websocket, EndPoint endpoint, WebSocketBuffers buffers, long timestamp, int maxIdleTime, int draft) throws IOException { // TODO - can we use the endpoint idle mechanism? @@ -48,32 +49,35 @@ public class WebSocketConnection implements Connection, WebSocket.Outbound _timestamp = timestamp; _websocket = websocket; - _generator = new WebSocketGenerator(buffers, _endp); - _parser = new WebSocketParser(buffers, endpoint, new WebSocketParser.EventHandler() + final WebSocketParser.FrameHandler handler = new WebSocketParser.FrameHandler() { - public void onFrame(byte frame, String data) - { - try - { - _websocket.onMessage(frame,data); - } - catch(ThreadDeath th) - { - throw th; - } - catch(Throwable th) - { - Log.warn(th); - } - } - - public void onFrame(byte frame, Buffer buffer) + Utf8StringBuilder _utf8 = new Utf8StringBuilder(); + + public void onFrame(boolean more, byte flags, byte opcode, Buffer buffer) { try { byte[] array=buffer.array(); - - _websocket.onMessage(frame,array,buffer.getIndex(),buffer.length()); + + if (opcode==0) + { + if (more) + _utf8.append(buffer.array(),buffer.getIndex(),buffer.length()); + else if (_utf8.length()==0) + _websocket.onMessage(opcode,buffer.toString("utf-8")); + else + { + _utf8.append(buffer.array(),buffer.getIndex(),buffer.length()); + _websocket.onMessage(opcode,_utf8.toString()); + _utf8.reset(); + } + } + else + { + if (more) + throw new IllegalStateException("not implemented"); + _websocket.onMessage(opcode,array,buffer.getIndex(),buffer.length()); + } } catch(ThreadDeath th) { @@ -84,7 +88,19 @@ public class WebSocketConnection implements Connection, WebSocket.Outbound Log.warn(th); } } - }); + }; + + // Select the parser/generators to use + switch(draft) + { + case 1: + _generator = new WebSocketGeneratorD01(buffers, _endp); + _parser = new WebSocketParserD01(buffers, endpoint, handler); + break; + default: + _generator = new WebSocketGeneratorD00(buffers, _endp); + _parser = new WebSocketParserD00(buffers, endpoint, handler); + } // TODO should these be AsyncEndPoint checks/calls? if (_endp instanceof SelectChannelEndPoint) @@ -205,9 +221,9 @@ public class WebSocketConnection implements Connection, WebSocket.Outbound private void doTheHixieHixieShake() { - byte[] result=WebSocketGenerator.doTheHixieHixieShake( - WebSocketParser.hixieCrypt(_key1), - WebSocketParser.hixieCrypt(_key2), + byte[] result=WebSocketConnection.doTheHixieHixieShake( + WebSocketConnection.hixieCrypt(_key1), + WebSocketConnection.hixieCrypt(_key2), _hixie.asArray()); _hixie.clear(); _hixie.put(result); @@ -284,6 +300,51 @@ public class WebSocketConnection implements Connection, WebSocket.Outbound ((AsyncEndPoint)_endp).scheduleWrite(); } + /* ------------------------------------------------------------ */ + static long hixieCrypt(String key) + { + // Don't ask me what all this is about. + // I think it's pretend secret stuff, kind of + // like talking in pig latin! + long number=0; + int spaces=0; + for (char c : key.toCharArray()) + { + if (Character.isDigit(c)) + number=number*10+(c-'0'); + else if (c==' ') + spaces++; + } + return number/spaces; + } + + public static byte[] doTheHixieHixieShake(long key1,long key2,byte[] key3) + { + try + { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte [] fodder = new byte[16]; + + fodder[0]=(byte)(0xff&(key1>>24)); + fodder[1]=(byte)(0xff&(key1>>16)); + fodder[2]=(byte)(0xff&(key1>>8)); + fodder[3]=(byte)(0xff&key1); + fodder[4]=(byte)(0xff&(key2>>24)); + fodder[5]=(byte)(0xff&(key2>>16)); + fodder[6]=(byte)(0xff&(key2>>8)); + fodder[7]=(byte)(0xff&key2); + for (int i=0;i<8;i++) + fodder[8+i]=key3[i]; + md.update(fodder); + byte[] result=md.digest(); + return result; + } + catch (NoSuchAlgorithmException e) + { + throw new IllegalStateException(e); + } + } + private interface IdleCheck { void access(EndPoint endp); diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketFactory.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketFactory.java index 37f5de71d09..c41cb950b21 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketFactory.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketFactory.java @@ -88,9 +88,10 @@ public class WebSocketFactory if (!"HTTP/1.1".equals(request.getProtocol())) throw new IllegalStateException("!HTTP/1.1"); + int draft=request.getIntHeader("Sec-WebSocket-Draft"); HttpConnection http = HttpConnection.getCurrentConnection(); ConnectedEndPoint endp = (ConnectedEndPoint)http.getEndPoint(); - WebSocketConnection connection = new WebSocketConnection(websocket,endp,_buffers,http.getTimeStamp(), _maxIdleTime); + WebSocketConnection connection = new WebSocketConnection(websocket,endp,_buffers,http.getTimeStamp(), _maxIdleTime,draft); String uri=request.getRequestURI(); String query=request.getQueryString(); diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketGenerator.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketGenerator.java index 3666b3b07d3..a081b1061a7 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketGenerator.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketGenerator.java @@ -1,197 +1,18 @@ package org.eclipse.jetty.websocket; import java.io.IOException; -import java.math.BigInteger; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import org.eclipse.jetty.io.Buffer; -import org.eclipse.jetty.io.EndPoint; -import org.eclipse.jetty.io.EofException; -import org.eclipse.jetty.util.TypeUtil; -import org.eclipse.jetty.util.log.Log; /* ------------------------------------------------------------ */ /** WebSocketGenerator. - * This class generates websocket packets. - * It is fully synchronized because it is likely that async - * threads will call the addMessage methods while other - * threads are flushing the generator. */ -public class WebSocketGenerator +public interface WebSocketGenerator { - final private WebSocketBuffers _buffers; - final private EndPoint _endp; - private Buffer _buffer; - - public WebSocketGenerator(WebSocketBuffers buffers, EndPoint endp) - { - _buffers=buffers; - _endp=endp; - } - - public synchronized void addFrame(byte frame,byte[] content, int blockFor) throws IOException - { - addFrame(frame,content,0,content.length,blockFor); - } - - public synchronized void addFrame(byte frame,byte[] content, int offset, int length, int blockFor) throws IOException - { - if (_buffer==null) - _buffer=_buffers.getDirectBuffer(); - - if (_buffer.space() == 0) - expelBuffer(blockFor); - - bufferPut(frame, blockFor); - - if (isLengthFrame(frame)) - { - // Send a length delimited frame - - // 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) - { - byte lengthByte = (byte)(0x80 | (0x7F & (length >> 7 * i))); - bufferPut(lengthByte, blockFor); - } - bufferPut((byte)(0x7F & length), 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 (!isLengthFrame(frame)) - _buffer.put((byte)0xFF); - // Gently flush the data, issuing a non-blocking write - flushBuffer(); - } - else - { - // Forcibly flush the data, issuing a blocking write - expelBuffer(blockFor); - if (remaining == 0) - { - if (!isLengthFrame(frame)) - _buffer.put((byte)0xFF); - // Gently flush the data, issuing a non-blocking write - flushBuffer(); - } - } - } - } - - private synchronized boolean isLengthFrame(byte frame) - { - return (frame & WebSocket.LENGTH_FRAME) == WebSocket.LENGTH_FRAME; - } - - private synchronized void bufferPut(byte datum, long blockFor) throws IOException - { - if (_buffer==null) - _buffer=_buffers.getDirectBuffer(); - _buffer.put(datum); - if (_buffer.space() == 0) - expelBuffer(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); - } - - public synchronized int flush(long blockFor) throws IOException - { - return expelBuffer(blockFor); - } - - public synchronized int flush() throws IOException - { - int flushed = flushBuffer(); - if (_buffer!=null && _buffer.length()==0) - { - _buffers.returnBuffer(_buffer); - _buffer=null; - } - return flushed; - } - - private synchronized int flushBuffer() throws IOException - { - if (!_endp.isOpen()) - throw new EofException(); - - if (_buffer!=null) - return _endp.flush(_buffer); - - return 0; - } - - private synchronized int expelBuffer(long blockFor) throws IOException - { - if (_buffer==null) - return 0; - int result = 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"); - - result += flushBuffer(); - _buffer.compact(); - } - } - return result; - } - - public synchronized boolean isBufferEmpty() - { - return _buffer==null || _buffer.length()==0; - } - - public static byte[] doTheHixieHixieShake(long key1,long key2,byte[] key3) - { - try - { - MessageDigest md = MessageDigest.getInstance("MD5"); - byte [] fodder = new byte[16]; - - fodder[0]=(byte)(0xff&(key1>>24)); - fodder[1]=(byte)(0xff&(key1>>16)); - fodder[2]=(byte)(0xff&(key1>>8)); - fodder[3]=(byte)(0xff&key1); - fodder[4]=(byte)(0xff&(key2>>24)); - fodder[5]=(byte)(0xff&(key2>>16)); - fodder[6]=(byte)(0xff&(key2>>8)); - fodder[7]=(byte)(0xff&key2); - for (int i=0;i<8;i++) - fodder[8+i]=key3[i]; - md.update(fodder); - byte[] result=md.digest(); - return result; - } - catch (NoSuchAlgorithmException e) - { - throw new IllegalStateException(e); - } - } - + int flush() throws IOException; + boolean isBufferEmpty(); + void addFrame(byte opcode, String content, int maxIdleTime) throws IOException; + void addFrame(byte opcode, byte[] content, int offset, int length, int maxIdleTime) throws IOException; + void addFrame(byte opcode, byte[] content, int maxIdleTime)throws IOException; + int flush(int maxIdleTime) throws IOException; } diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketGeneratorD00.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketGeneratorD00.java new file mode 100644 index 00000000000..94925f576fe --- /dev/null +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketGeneratorD00.java @@ -0,0 +1,168 @@ +package org.eclipse.jetty.websocket; + +import java.io.IOException; +import java.math.BigInteger; + +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.util.log.Log; + + +/* ------------------------------------------------------------ */ +/** WebSocketGenerator. + * This class generates websocket packets. + * It is fully synchronized because it is likely that async + * threads will call the addMessage methods while other + * threads are flushing the generator. + */ +public class WebSocketGeneratorD00 implements WebSocketGenerator +{ + final private WebSocketBuffers _buffers; + final private EndPoint _endp; + private Buffer _buffer; + + public WebSocketGeneratorD00(WebSocketBuffers buffers, EndPoint endp) + { + _buffers=buffers; + _endp=endp; + } + + public synchronized void addFrame(byte frame,byte[] content, int blockFor) throws IOException + { + addFrame(frame,content,0,content.length,blockFor); + } + + public synchronized void addFrame(byte opcode,byte[] content, int offset, int length, int blockFor) throws IOException + { + if (_buffer==null) + _buffer=_buffers.getDirectBuffer(); + + if (_buffer.space() == 0) + expelBuffer(blockFor); + + bufferPut(opcode, blockFor); + + if (isLengthFrame(opcode)) + { + // Send a length delimited frame + + // 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) + { + byte lengthByte = (byte)(0x80 | (0x7F & (length >> 7 * i))); + bufferPut(lengthByte, blockFor); + } + bufferPut((byte)(0x7F & length), 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 (!isLengthFrame(opcode)) + _buffer.put((byte)0xFF); + // Gently flush the data, issuing a non-blocking write + flushBuffer(); + } + else + { + // Forcibly flush the data, issuing a blocking write + expelBuffer(blockFor); + if (remaining == 0) + { + if (!isLengthFrame(opcode)) + _buffer.put((byte)0xFF); + // Gently flush the data, issuing a non-blocking write + flushBuffer(); + } + } + } + } + + private synchronized boolean isLengthFrame(byte frame) + { + return (frame & WebSocket.LENGTH_FRAME) == WebSocket.LENGTH_FRAME; + } + + private synchronized void bufferPut(byte datum, long blockFor) throws IOException + { + if (_buffer==null) + _buffer=_buffers.getDirectBuffer(); + _buffer.put(datum); + if (_buffer.space() == 0) + expelBuffer(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); + } + + public synchronized int flush(int blockFor) throws IOException + { + return expelBuffer(blockFor); + } + + public synchronized int flush() throws IOException + { + int flushed = flushBuffer(); + if (_buffer!=null && _buffer.length()==0) + { + _buffers.returnBuffer(_buffer); + _buffer=null; + } + return flushed; + } + + private synchronized int flushBuffer() throws IOException + { + if (!_endp.isOpen()) + throw new EofException(); + + if (_buffer!=null) + return _endp.flush(_buffer); + + return 0; + } + + private synchronized int expelBuffer(long blockFor) throws IOException + { + if (_buffer==null) + return 0; + int result = 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"); + + result += flushBuffer(); + _buffer.compact(); + } + } + return result; + } + + public synchronized boolean isBufferEmpty() + { + return _buffer==null || _buffer.length()==0; + } + +} diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketGeneratorD01.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketGeneratorD01.java new file mode 100644 index 00000000000..485bd2e3ea5 --- /dev/null +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketGeneratorD01.java @@ -0,0 +1,181 @@ +package org.eclipse.jetty.websocket; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.util.log.Log; + + +/* ------------------------------------------------------------ */ +/** WebSocketGenerator. + * This class generates websocket packets. + * It is fully synchronized because it is likely that async + * threads will call the addMessage methods while other + * threads are flushing the generator. + */ +public class WebSocketGeneratorD01 implements WebSocketGenerator +{ + final private WebSocketBuffers _buffers; + final private EndPoint _endp; + private Buffer _buffer; + + public WebSocketGeneratorD01(WebSocketBuffers buffers, EndPoint endp) + { + _buffers=buffers; + _endp=endp; + } + + public synchronized void addFrame(byte opcode,byte[] content, int blockFor) throws IOException + { + addFrame(opcode,content,0,content.length,blockFor); + } + + public synchronized void addFrame(byte opcode,byte[] content, int offset, int length, int blockFor) throws IOException + { + if (_buffer==null) + _buffer=_buffers.getDirectBuffer(); + + if (_buffer.space() == 0) + expelBuffer(blockFor); + + opcode = (byte)(opcode & 0x0f); + + while (length>0) + { + // slice a fragment off + int fragment=length; + if (fragment+10>_buffer.capacity()) + { + fragment=_buffer.capacity()-10; + bufferPut((byte)(0x80|opcode), blockFor); + } + else + bufferPut(opcode, blockFor); + + if (fragment>0xffff) + { + bufferPut((byte)127, blockFor); + bufferPut((byte)((fragment>>56)&0x7f), blockFor); + bufferPut((byte)((fragment>>48)&0xff), blockFor); + bufferPut((byte)((fragment>>40)&0xff), blockFor); + bufferPut((byte)((fragment>>32)&0xff), blockFor); + bufferPut((byte)((fragment>>24)&0xff), blockFor); + bufferPut((byte)((fragment>>16)&0xff), blockFor); + bufferPut((byte)((fragment>>8)&0xff), blockFor); + bufferPut((byte)(fragment&0xff), blockFor); + } + else if (fragment >=0x7e) + { + bufferPut((byte)126, blockFor); + bufferPut((byte)(fragment>>8), blockFor); + bufferPut((byte)(fragment&0xff), blockFor); + } + else + { + bufferPut((byte)fragment, blockFor); + } + + int remaining = fragment; + while (remaining > 0) + { + _buffer.compact(); + int chunk = remaining < _buffer.space() ? remaining : _buffer.space(); + _buffer.put(content, offset + (fragment - remaining), chunk); + remaining -= chunk; + if (_buffer.space() > 0) + { + // Gently flush the data, issuing a non-blocking write + flushBuffer(); + } + else + { + // Forcibly flush the data, issuing a blocking write + expelBuffer(blockFor); + if (remaining == 0) + { + // Gently flush the data, issuing a non-blocking write + flushBuffer(); + } + } + } + offset+=fragment; + length-=fragment; + } + } + + private synchronized void bufferPut(byte datum, long blockFor) throws IOException + { + if (_buffer==null) + _buffer=_buffers.getDirectBuffer(); + _buffer.put(datum); + if (_buffer.space() == 0) + expelBuffer(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); + } + + public synchronized int flush(int blockFor) throws IOException + { + return expelBuffer(blockFor); + } + + public synchronized int flush() throws IOException + { + int flushed = flushBuffer(); + if (_buffer!=null && _buffer.length()==0) + { + _buffers.returnBuffer(_buffer); + _buffer=null; + } + return flushed; + } + + private synchronized int flushBuffer() throws IOException + { + if (!_endp.isOpen()) + throw new EofException(); + + if (_buffer!=null) + return _endp.flush(_buffer); + + return 0; + } + + private synchronized int expelBuffer(long blockFor) throws IOException + { + if (_buffer==null) + return 0; + int result = 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"); + + result += flushBuffer(); + _buffer.compact(); + } + } + return result; + } + + public synchronized boolean isBufferEmpty() + { + return _buffer==null || _buffer.length()==0; + } +} diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketParser.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketParser.java index 49449fbe521..5c0b9b45ee9 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketParser.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketParser.java @@ -1,13 +1,6 @@ package org.eclipse.jetty.websocket; -import java.io.IOException; - import org.eclipse.jetty.io.Buffer; -import org.eclipse.jetty.io.Buffers; -import org.eclipse.jetty.io.EndPoint; -import org.eclipse.jetty.util.Utf8StringBuilder; -import org.eclipse.jetty.util.log.Log; - @@ -16,207 +9,23 @@ import org.eclipse.jetty.util.log.Log; * Parser the WebSocket protocol. * */ -public class WebSocketParser +public interface WebSocketParser { - public static final int STATE_START=0; - public static final int STATE_SENTINEL_DATA=1; - public static final int STATE_LENGTH=2; - public static final int STATE_DATA=3; - - private final WebSocketBuffers _buffers; - private final EndPoint _endp; - private final EventHandler _handler; - private int _state; - private Buffer _buffer; - private byte _frame; - private int _length; - private Utf8StringBuilder _utf8; - - /* ------------------------------------------------------------ */ - /** - * @param buffers The buffers to use for parsing. Only the {@link Buffers#getBuffer()} is used. - * This should be a direct buffer if binary data is mostly used or an indirect buffer if utf-8 data - * is mostly used. - * @param endp - * @param handler - */ - public WebSocketParser(WebSocketBuffers buffers, EndPoint endp, EventHandler handler) - { - _buffers=buffers; - _endp=endp; - _handler=handler; - } - - /* ------------------------------------------------------------ */ - public boolean isBufferEmpty() - { - return _buffer==null || _buffer.length()==0; - } - - /* ------------------------------------------------------------ */ - public Buffer getBuffer() - { - return _buffer; - } - - /* ------------------------------------------------------------ */ - /** Parse to next event. - * Parse to the next {@link EventHandler} event or until no more data is - * available. Fill data from the {@link EndPoint} only as necessary. - * @return An indication of progress or otherwise. -1 indicates EOF, 0 indicates - * that no bytes were read and no messages parsed. A positive number indicates either - * the bytes filled or the messages parsed. - */ - public int parseNext() - { - if (_buffer==null) - _buffer=_buffers.getBuffer(); - - int total_filled=0; - - // Loop until an datagram call back or can't fill anymore - boolean progress=true; - while(true) - { - int length=_buffer.length(); - - // Fill buffer if we need a byte or need length - if (length == 0 || _state==STATE_DATA && length<_length) - { - // compact to mark (set at start of data) - _buffer.compact(); - - // if no space, then the data is too big for buffer - if (_buffer.space() == 0) - throw new IllegalStateException("FULL"); - - // catch IOExceptions (probably EOF) and try to parse what we have - try - { - int filled=_endp.isOpen()?_endp.fill(_buffer):-1; - if (filled<=0) - return total_filled; - total_filled+=filled; - length=_buffer.length(); - } - catch(IOException e) - { - Log.debug(e); - return total_filled>0?total_filled:-1; - } - } - - - // Parse the buffer byte by byte (unless it is STATE_DATA) - byte b; - charloop: while (length-->0) - { - switch (_state) - { - case STATE_START: - b=_buffer.get(); - _frame=b; - if (_frame<0) - { - _length=0; - _state=STATE_LENGTH; - } - else - { - if (_utf8==null) - _utf8=new Utf8StringBuilder(); - _state=STATE_SENTINEL_DATA; - _buffer.mark(); - } - continue; - - case STATE_SENTINEL_DATA: - b=_buffer.get(); - if ((b&0xff)==0xff) - { - String data=_utf8.toString(); - _utf8.reset(); - _state=STATE_START; - _handler.onFrame(_frame,data); - _buffer.setMarkIndex(-1); - if (_buffer.length()==0) - { - _buffers.returnBuffer(_buffer); - _buffer=null; - } - return total_filled; - } - _utf8.append(b); - continue; - - case STATE_LENGTH: - b=_buffer.get(); - _length=_length<<7 | (0x7f&b); - if (b>=0) - { - _state=STATE_DATA; - _buffer.mark(0); - } - continue; - - case STATE_DATA: - if (_buffer.markIndex()<0) - if (_buffer.length()<_length) - break charloop; - Buffer data=_buffer.sliceFromMark(_length); - _buffer.skip(_length); - _state=STATE_START; - _handler.onFrame(_frame,data); - - if (_buffer.length()==0) - { - _buffers.returnBuffer(_buffer); - _buffer=null; - } - - return total_filled; - } - } - } - } - - /* ------------------------------------------------------------ */ - public void fill(Buffer buffer) - { - if (buffer!=null && buffer.length()>0) - { - if (_buffer==null) - _buffer=_buffers.getBuffer(); - _buffer.put(buffer); - buffer.clear(); - } - } - - /* ------------------------------------------------------------ */ - static long hixieCrypt(String key) - { - // Don't ask me what all this is about. - // I think it's pretend secret stuff, kind of - // like talking in pig latin! - long number=0; - int spaces=0; - for (char c : key.toCharArray()) - { - if (Character.isDigit(c)) - number=number*10+(c-'0'); - else if (c==' ') - spaces++; - } - return number/spaces; - } /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ - public interface EventHandler + public interface FrameHandler { - void onFrame(byte frame,String data); - void onFrame(byte frame,Buffer buffer); + void onFrame(boolean more,byte flags, byte opcode, Buffer buffer); } + Buffer getBuffer(); + + int parseNext(); + + boolean isBufferEmpty(); + + void fill(Buffer buffer); + } diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketParserD00.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketParserD00.java new file mode 100644 index 00000000000..c7ecb091276 --- /dev/null +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketParserD00.java @@ -0,0 +1,191 @@ +package org.eclipse.jetty.websocket; + +import java.io.IOException; + +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.Buffers; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.Utf8StringBuilder; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.websocket.WebSocketParser.FrameHandler; + + + + +/* ------------------------------------------------------------ */ +/** + * Parser the WebSocket protocol. + * + */ +public class WebSocketParserD00 implements WebSocketParser +{ + public static final int STATE_START=0; + public static final int STATE_SENTINEL_DATA=1; + public static final int STATE_LENGTH=2; + public static final int STATE_DATA=3; + + private final WebSocketBuffers _buffers; + private final EndPoint _endp; + private final FrameHandler _handler; + private int _state; + private Buffer _buffer; + private byte _opcode; + private int _length; + + /* ------------------------------------------------------------ */ + /** + * @param buffers The buffers to use for parsing. Only the {@link Buffers#getBuffer()} is used. + * This should be a direct buffer if binary data is mostly used or an indirect buffer if utf-8 data + * is mostly used. + * @param endp + * @param handler + */ + public WebSocketParserD00(WebSocketBuffers buffers, EndPoint endp, FrameHandler handler) + { + _buffers=buffers; + _endp=endp; + _handler=handler; + } + + /* ------------------------------------------------------------ */ + public boolean isBufferEmpty() + { + return _buffer==null || _buffer.length()==0; + } + + /* ------------------------------------------------------------ */ + public Buffer getBuffer() + { + return _buffer; + } + + /* ------------------------------------------------------------ */ + /** Parse to next event. + * Parse to the next {@link FrameHandler} event or until no more data is + * available. Fill data from the {@link EndPoint} only as necessary. + * @return An indication of progress or otherwise. -1 indicates EOF, 0 indicates + * that no bytes were read and no messages parsed. A positive number indicates either + * the bytes filled or the messages parsed. + */ + public int parseNext() + { + if (_buffer==null) + _buffer=_buffers.getBuffer(); + + int total_filled=0; + + // Loop until an datagram call back or can't fill anymore + boolean progress=true; + while(true) + { + int length=_buffer.length(); + + // Fill buffer if we need a byte or need length + if (length == 0 || _state==STATE_DATA && length<_length) + { + // compact to mark (set at start of data) + _buffer.compact(); + + // if no space, then the data is too big for buffer + if (_buffer.space() == 0) + throw new IllegalStateException("FULL"); + + // catch IOExceptions (probably EOF) and try to parse what we have + try + { + int filled=_endp.isOpen()?_endp.fill(_buffer):-1; + if (filled<=0) + return total_filled; + total_filled+=filled; + length=_buffer.length(); + } + catch(IOException e) + { + Log.debug(e); + return total_filled>0?total_filled:-1; + } + } + + + // Parse the buffer byte by byte (unless it is STATE_DATA) + byte b; + charloop: while (length-->0) + { + switch (_state) + { + case STATE_START: + b=_buffer.get(); + _opcode=b; + if (_opcode<0) + { + _length=0; + _state=STATE_LENGTH; + } + else + { + _state=STATE_SENTINEL_DATA; + _buffer.mark(0); + } + continue; + + case STATE_SENTINEL_DATA: + b=_buffer.get(); + if ((b&0xff)==0xff) + { + _state=STATE_START; + int l=_buffer.getIndex()-_buffer.markIndex()-1; + _handler.onFrame(false,(byte)0,_opcode,_buffer.sliceFromMark(l)); + _buffer.setMarkIndex(-1); + if (_buffer.length()==0) + { + _buffers.returnBuffer(_buffer); + _buffer=null; + } + return total_filled; + } + continue; + + case STATE_LENGTH: + b=_buffer.get(); + _length=_length<<7 | (0x7f&b); + if (b>=0) + { + _state=STATE_DATA; + _buffer.mark(0); + } + continue; + + case STATE_DATA: + if (_buffer.markIndex()<0) + if (_buffer.length()<_length) + break charloop; + Buffer data=_buffer.sliceFromMark(_length); + _buffer.skip(_length); + _state=STATE_START; + _handler.onFrame(false,(byte)0, _opcode, data); + + if (_buffer.length()==0) + { + _buffers.returnBuffer(_buffer); + _buffer=null; + } + + return total_filled; + } + } + } + } + + /* ------------------------------------------------------------ */ + public void fill(Buffer buffer) + { + if (buffer!=null && buffer.length()>0) + { + if (_buffer==null) + _buffer=_buffers.getBuffer(); + _buffer.put(buffer); + buffer.clear(); + } + } + +} diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketParserD01.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketParserD01.java new file mode 100644 index 00000000000..6b9d955775d --- /dev/null +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketParserD01.java @@ -0,0 +1,214 @@ +package org.eclipse.jetty.websocket; + +import java.io.IOException; + +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.Buffers; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.Utf8StringBuilder; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.websocket.WebSocketParser.FrameHandler; + + + +/* ------------------------------------------------------------ */ +/** + * Parser the WebSocket protocol. + * + */ +public class WebSocketParserD01 implements WebSocketParser +{ + public enum State { + START(1), LENGTH_7(2), LENGTH_16(4), LENGTH_63(10), DATA(10); + + int _minSize; + + State(int minSize) + { + _minSize=minSize; + } + + int getMinSize() + { + return _minSize; + } + }; + + + private final WebSocketBuffers _buffers; + private final EndPoint _endp; + private final FrameHandler _handler; + private State _state=State.START; + private Buffer _buffer; + private boolean _more; + private byte _flags; + private byte _opcode; + private int _count; + private long _length; + private Utf8StringBuilder _utf8; + + /* ------------------------------------------------------------ */ + /** + * @param buffers The buffers to use for parsing. Only the {@link Buffers#getBuffer()} is used. + * This should be a direct buffer if binary data is mostly used or an indirect buffer if utf-8 data + * is mostly used. + * @param endp + * @param handler + */ + public WebSocketParserD01(WebSocketBuffers buffers, EndPoint endp, FrameHandler handler) + { + _buffers=buffers; + _endp=endp; + _handler=handler; + } + + /* ------------------------------------------------------------ */ + public boolean isBufferEmpty() + { + return _buffer==null || _buffer.length()==0; + } + + /* ------------------------------------------------------------ */ + public Buffer getBuffer() + { + return _buffer; + } + + /* ------------------------------------------------------------ */ + /** Parse to next event. + * Parse to the next {@link FrameHandler} event or until no more data is + * available. Fill data from the {@link EndPoint} only as necessary. + * @return An indication of progress or otherwise. -1 indicates EOF, 0 indicates + * that no bytes were read and no messages parsed. A positive number indicates either + * the bytes filled or the messages parsed. + */ + public int parseNext() + { + if (_buffer==null) + _buffer=_buffers.getBuffer(); + + int total_filled=0; + + // Loop until an datagram call back or can't fill anymore + while(true) + { + int available=_buffer.length(); + + // Fill buffer if we need a byte or need length + if (available < _state.getMinSize() || _state==State.DATA && available<_count) + { + // compact to mark (set at start of data) + _buffer.compact(); + + // if no space, then the data is too big for buffer + if (_buffer.space() == 0) + throw new IllegalStateException("FULL"); + + // catch IOExceptions (probably EOF) and try to parse what we have + try + { + int filled=_endp.isOpen()?_endp.fill(_buffer):-1; + if (filled<=0) + return total_filled; + total_filled+=filled; + available=_buffer.length(); + } + catch(IOException e) + { + Log.debug(e); + return total_filled>0?total_filled:-1; + } + } + + // Parse the buffer byte by byte (unless it is STATE_DATA) + byte b; + while (_state!=State.DATA && available-->0) + { + switch (_state) + { + case START: + b=_buffer.get(); + _opcode=(byte)(b&0xf); + _flags=(byte)(b>>4); + _more=(_flags&8)!=0; + _state=State.LENGTH_7; + continue; + + case LENGTH_7: + b=_buffer.get(); + switch(b) + { + case 127: + _length=0; + _count=8; + _state=State.LENGTH_63; + break; + case 126: + _length=0; + _count=2; + _state=State.LENGTH_16; + break; + default: + _length=(0x7f&b); + _count=(int)_length; + _state=State.DATA; + } + continue; + + case LENGTH_16: + b=_buffer.get(); + _length = _length<<8 | b; + if (--_count==0) + { + if (_length>=_buffer.capacity()-4) + throw new IllegalStateException("TOO LARGE"); + _count=(int)_length; + _state=State.DATA; + } + continue; + + case LENGTH_63: + b=_buffer.get(); + _length = _length<<8 | b; + if (--_count==0) + { + if (_length>=_buffer.capacity()-10) + throw new IllegalStateException("TOO LARGE"); + _count=(int)_length; + _state=State.DATA; + } + continue; + } + } + + if (_state==State.DATA && available>=_count) + { + _handler.onFrame(_more,_flags, _opcode, _buffer.get(_count)); + _count=0; + _state=State.START; + + if (_buffer.length()==0) + { + _buffers.returnBuffer(_buffer); + _buffer=null; + } + + return total_filled; + + } + } + } + + /* ------------------------------------------------------------ */ + public void fill(Buffer buffer) + { + if (buffer!=null && buffer.length()>0) + { + if (_buffer==null) + _buffer=_buffers.getBuffer(); + _buffer.put(buffer); + buffer.clear(); + } + } + +} diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketGeneratorD00Test.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketGeneratorD00Test.java new file mode 100644 index 00000000000..f31bf14aebd --- /dev/null +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketGeneratorD00Test.java @@ -0,0 +1,112 @@ +package org.eclipse.jetty.websocket; + +import static junit.framework.Assert.assertEquals; + +import org.eclipse.jetty.io.ByteArrayBuffer; +import org.eclipse.jetty.io.ByteArrayEndPoint; +import org.junit.Before; +import org.junit.Test; + +/** + * @version $Revision: 1441 $ $Date: 2010-04-02 12:28:17 +0200 (Fri, 02 Apr 2010) $ + */ +public class WebSocketGeneratorD00Test +{ + private ByteArrayBuffer _out; + private WebSocketGenerator _generator; + + @Before + public void setUp() throws Exception + { + WebSocketBuffers buffers = new WebSocketBuffers(1024); + ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); + _generator = new WebSocketGeneratorD01(buffers, endPoint); + _out = new ByteArrayBuffer(4096); + endPoint.setOut(_out); + } + + @Test + public void testOneString() throws Exception + { + _generator.addFrame((byte)0x04,"Hell\uFF4F W\uFF4Frld",0); + _generator.flush(); + assertEquals(4,_out.get()); + assertEquals(15,_out.get()); + assertEquals('H',_out.get()); + assertEquals('e',_out.get()); + assertEquals('l',_out.get()); + assertEquals('l',_out.get()); + assertEquals(0xEF,0xff&_out.get()); + assertEquals(0xBD,0xff&_out.get()); + assertEquals(0x8F,0xff&_out.get()); + assertEquals(' ',_out.get()); + assertEquals('W',_out.get()); + assertEquals(0xEF,0xff&_out.get()); + assertEquals(0xBD,0xff&_out.get()); + assertEquals(0x8F,0xff&_out.get()); + assertEquals('r',_out.get()); + assertEquals('l',_out.get()); + assertEquals('d',_out.get()); + } + + @Test + public void testOneMediumBuffer() throws Exception + { + byte[] b=new byte[501]; + for (int i=0;i>8),0xff&_out.get()); + assertEquals(0xff&b.length,0xff&_out.get()); + for (int i=0;i _data = new ArrayList(); - public void onFrame(byte frame, Buffer buffer) + public void onFrame(boolean more, byte flags, byte opcode, Buffer buffer) { _data.add(buffer.toString(StringUtil.__UTF8)); } - - public void onFrame(byte frame, String data) - { - _data.add(data); - } } } diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketParserD01Test.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketParserD01Test.java new file mode 100644 index 00000000000..54b032e0508 --- /dev/null +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketParserD01Test.java @@ -0,0 +1,187 @@ +package org.eclipse.jetty.websocket; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jetty.http.HttpHeaderValues; +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.BufferCache.CachedBuffer; +import org.eclipse.jetty.io.ByteArrayBuffer; +import org.eclipse.jetty.io.ByteArrayEndPoint; +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.Utf8StringBuilder; +import org.junit.Before; +import org.junit.Test; + +/** + * @version $Revision: 1441 $ $Date: 2010-04-02 12:28:17 +0200 (Fri, 02 Apr 2010) $ + */ +public class WebSocketParserD01Test +{ + private ByteArrayBuffer _in; + private Handler _handler; + private WebSocketParser _parser; + + @Before + public void setUp() throws Exception + { + WebSocketBuffers buffers = new WebSocketBuffers(1024); + ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); + _handler = new Handler(); + _parser=new WebSocketParserD01(buffers, endPoint,_handler); + _in = new ByteArrayBuffer(2048); + endPoint.setIn(_in); + } + + @Test + public void testCache() throws Exception + { + assertEquals(HttpHeaderValues.UPGRADE_ORDINAL ,((CachedBuffer)HttpHeaderValues.CACHE.lookup("Upgrade")).getOrdinal()); + } + + @Test + public void testShortText() throws Exception + { + _in.put((byte)0x00); + _in.put((byte)11); + _in.put("Hello World".getBytes(StringUtil.__UTF8)); + + int filled =_parser.parseNext(); + + assertEquals(13,filled); + assertEquals("Hello World",_handler._data.get(0)); + assertTrue(_parser.isBufferEmpty()); + assertTrue(_parser.getBuffer()==null); + } + + @Test + public void testShortUtf8() throws Exception + { + String string = "Hell\uFF4f W\uFF4Frld"; + byte[] bytes = string.getBytes("UTF-8"); + + _in.put((byte)0x00); + _in.put((byte)bytes.length); + _in.put(bytes); + + int filled =_parser.parseNext(); + + assertEquals(bytes.length+2,filled); + assertEquals(string,_handler._data.get(0)); + assertTrue(_parser.isBufferEmpty()); + assertTrue(_parser.getBuffer()==null); + } + + @Test + public void testMediumText() throws Exception + { + String string = "Hell\uFF4f Medium W\uFF4Frld "; + for (int i=0;i<4;i++) + string = string+string; + string += ". The end."; + + byte[] bytes = string.getBytes("UTF-8"); + + _in.put((byte)0x00); + _in.put((byte)0x7E); + _in.put((byte)(bytes.length>>8)); + _in.put((byte)(bytes.length&0xff)); + _in.put(bytes); + + int filled =_parser.parseNext(); + + assertEquals(bytes.length+4,filled); + assertEquals(string,_handler._data.get(0)); + assertTrue(_parser.isBufferEmpty()); + assertTrue(_parser.getBuffer()==null); + } + + @Test + public void testLongText() throws Exception + { + + WebSocketBuffers buffers = new WebSocketBuffers(0x20000); + ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); + WebSocketParser parser=new WebSocketParserD01(buffers, endPoint,_handler); + ByteArrayBuffer in = new ByteArrayBuffer(0x20000); + endPoint.setIn(in); + + + String string = "Hell\uFF4f Big W\uFF4Frld "; + for (int i=0;i<12;i++) + string = string+string; + string += ". The end."; + + byte[] bytes = string.getBytes("UTF-8"); + System.err.println(Long.toHexString(bytes.length)); + + in.put((byte)0x00); + in.put((byte)0x7F); + in.put((byte)0x00); + in.put((byte)0x00); + in.put((byte)0x00); + in.put((byte)0x00); + in.put((byte)0x00); + in.put((byte)(bytes.length>>16)); + in.put((byte)((bytes.length>>8)&0xff)); + in.put((byte)(bytes.length&0xff)); + in.put(bytes); + + int filled =parser.parseNext(); + + assertEquals(bytes.length+10,filled); + assertEquals(string,_handler._data.get(0)); + assertTrue(parser.isBufferEmpty()); + assertTrue(parser.getBuffer()==null); + } + + @Test + public void testShortFragmentTest() throws Exception + { + _in.put((byte)0x80); + _in.put((byte)0x06); + _in.put("Hello ".getBytes(StringUtil.__UTF8)); + _in.put((byte)0x00); + _in.put((byte)0x05); + _in.put("World".getBytes(StringUtil.__UTF8)); + + int filled =_parser.parseNext(); + + assertEquals(15,filled); + assertEquals(0,_handler._data.size()); + assertFalse(_parser.isBufferEmpty()); + assertFalse(_parser.getBuffer()==null); + + filled =_parser.parseNext(); + + assertEquals(0,filled); + assertEquals("Hello World",_handler._data.get(0)); + assertTrue(_parser.isBufferEmpty()); + assertTrue(_parser.getBuffer()==null); + } + + + private class Handler implements WebSocketParser.FrameHandler + { + Utf8StringBuilder _utf8 = new Utf8StringBuilder(); + public List _data = new ArrayList(); + + public void onFrame(boolean more, byte flags, byte opcode, Buffer buffer) + { + if (more) + _utf8.append(buffer.array(),buffer.getIndex(),buffer.length()); + else if (_utf8.length()==0) + _data.add(opcode,buffer.toString("utf-8")); + else + { + _utf8.append(buffer.array(),buffer.getIndex(),buffer.length()); + _data.add(opcode,_utf8.toString()); + _utf8.reset(); + } + } + } +} diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketTest.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketTest.java index eb62f5a1eba..d9b52cce58f 100644 --- a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketTest.java +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketTest.java @@ -1,6 +1,11 @@ package org.eclipse.jetty.websocket; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + import java.io.IOException; +import java.security.MessageDigest; + import javax.servlet.http.HttpServletRequest; import org.eclipse.jetty.io.Buffer; @@ -9,14 +14,12 @@ import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.DefaultHandler; import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.log.Log; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - public class WebSocketTest { private static TestWebSocket _websocket; @@ -51,6 +54,44 @@ public class WebSocketTest _server.join(); } + @Test + public void testHixieCrypt() throws Exception + { + assertEquals(155712099,WebSocketConnection.hixieCrypt("18x 6]8vM;54 *(5: { U1]8 z [ 8")); + assertEquals(173347027,WebSocketConnection.hixieCrypt("1_ tx7X d < nw 334J702) 7]o}` 0")); + } + + @Test + public void testHixie() throws Exception + { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] result; + byte[] expected; + + expected=md.digest(TypeUtil.fromHexString("00000000000000000000000000000000")); + result=WebSocketConnection.doTheHixieHixieShake( + 0 ,0, new byte[8]); + assertEquals(TypeUtil.toHexString(expected),TypeUtil.toHexString(result)); + + expected=md.digest(TypeUtil.fromHexString("01020304050607080000000000000000")); + result=WebSocketConnection.doTheHixieHixieShake( + 0x01020304, + 0x05060708, + new byte[8]); + assertEquals(TypeUtil.toHexString(expected),TypeUtil.toHexString(result)); + + byte[] random = new byte[8]; + for (int i=0;i<8;i++) + random[i]=(byte)(0xff&"Tm[K T2u".charAt(i)); + result=WebSocketConnection.doTheHixieHixieShake( + 155712099,173347027,random); + StringBuilder b = new StringBuilder(); + + for (int i=0;i<16;i++) + b.append((char)result[i]); + assertEquals("fQJ,fN/4F4!~K~MH",b.toString()); + } + @Test public void testNoWebSocket() throws Exception { diff --git a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketTestServer.java b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketTestServer.java index e500b337c24..56c14c65660 100644 --- a/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketTestServer.java +++ b/jetty-websocket/src/test/java/org/eclipse/jetty/websocket/WebSocketTestServer.java @@ -2,6 +2,7 @@ package org.eclipse.jetty.websocket; import java.io.IOException; import java.util.concurrent.ConcurrentLinkedQueue; + import javax.servlet.http.HttpServletRequest; import org.eclipse.jetty.server.Server;