From 6ab98782ef3ca14f4b6bd051949b8193fe75d586 Mon Sep 17 00:00:00 2001 From: Nathan Mische Date: Fri, 23 Jul 2010 11:46:45 -0400 Subject: [PATCH 01/12] Updating server to support version 76 of the WebSocket specification. --- src/WebSocket.java | 16 +- src/WebSocketClient.java | 4 +- src/WebSocketServer.java | 335 +++++++++++++++++++++++++++++++++++---- 3 files changed, 321 insertions(+), 34 deletions(-) diff --git a/src/WebSocket.java b/src/WebSocket.java index 36e6a8fd2..b8483a3e9 100644 --- a/src/WebSocket.java +++ b/src/WebSocket.java @@ -179,11 +179,21 @@ private void recieveHandshake() throws IOException { // If the ByteBuffer ends with 0x0D 0x0A 0x0D 0x0A // (or two CRLFs), then the client handshake is complete + // (version 75) + + // If the ByteBuffer ends with 0x0D 0x0A 0x0D 0x0A + // followed by 8 random bytes, then the client + // handshake is complete (version 76) + byte[] h = this.remoteHandshake.array(); if ((h.length>=4 && h[h.length-4] == CR - && h[h.length-3] == LF - && h[h.length-2] == CR - && h[h.length-1] == LF) || + && h[h.length-3] == LF + && h[h.length-2] == CR + && h[h.length-1] == LF) || + (h.length>=8 && h[h.length-12] == CR + && h[h.length-11] == LF + && h[h.length-10] == CR + && h[h.length-9] == LF) || (h.length==23 && h[h.length-1] == 0)) { completeHandshake(); } diff --git a/src/WebSocketClient.java b/src/WebSocketClient.java index 2f8d651a7..bcfd551ce 100644 --- a/src/WebSocketClient.java +++ b/src/WebSocketClient.java @@ -120,10 +120,10 @@ public void run() { while (selector.select(500) > 0) { Set keys = selector.selectedKeys(); - Iterator i = keys.iterator(); + Iterator i = keys.iterator(); while (i.hasNext()) { - SelectionKey key = (SelectionKey)i.next(); + SelectionKey key = i.next(); i.remove(); // When 'conn' has connected to the host diff --git a/src/WebSocketServer.java b/src/WebSocketServer.java index ce85ddeb5..8a451494d 100644 --- a/src/WebSocketServer.java +++ b/src/WebSocketServer.java @@ -5,6 +5,8 @@ import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Iterator; import java.util.Properties; import java.util.Set; @@ -29,6 +31,7 @@ public abstract class WebSocketServer implements Runnable, WebSocketListener { */ public static final String FLASH_POLICY_REQUEST = "\0"; + public static final Long MAX_KEY_VALUE = Long.parseLong("4294967295"); // INSTANCE PROPERTIES ///////////////////////////////////////////////////// /** @@ -161,10 +164,10 @@ public void run() { while(true) { selector.select(); Set keys = selector.selectedKeys(); - Iterator i = keys.iterator(); + Iterator i = keys.iterator(); while(i.hasNext()) { - SelectionKey key = (SelectionKey) i.next(); + SelectionKey key = i.next(); // Remove the current key i.remove(); @@ -230,53 +233,327 @@ public boolean onHandshakeRecieved(WebSocket conn, String handshake) throws IOEx conn.socketChannel().write(ByteBuffer.wrap(policy.getBytes(WebSocket.UTF8_CHARSET))); return false; } + + String[] requestLines = handshake.split("\r\n"); + String line; + + Properties p = new Properties(); + for (int i=1; i MAX_KEY_VALUE || key2 > MAX_KEY_VALUE) { + return false; + } + + // 6.2.5. Let /spaces_1/ be the number of U+0020 SPACE characters in + // /key_1/. + + // Let /spaces_2/ be the number of U+0020 SPACE characters in + // /key_2/. + + // If either /spaces_1/ or /spaces_2/ is zero, then abort the + // WebSocket connection. This is a symptom of a cross-protocol + // attack. + + int spaces1 = p.getProperty("sec-websocket-key1").replaceAll("[^ ]", "").length(); + int spaces2 = p.getProperty("sec-websocket-key2").replaceAll("[^ ]", "").length(); + + if (spaces1 == 0 || spaces2 == 0) { + return false; + } + + // 6.2.6. If /key-number_1/ is not an integral multiple of /spaces_1/, + // then abort the WebSocket connection. + + // If /key-number_2/ is not an integral multiple of /spaces_2/, + // then abort the WebSocket connection. + + long mod1 = key1 % spaces1; + long mod2 = key2 % spaces2; + + if (mod1 != 0 || mod2 != 0) { + return false; + } + + // 2.6.7. Let /part_1/ be /key-number_1/ divided by /spaces_1/. + + // Let /part_2/ be /key-number_2/ divided by /spaces_2/. + + long part1 = key1/spaces1; + long part2 = key2/spaces2; + + // 2.6.8. Let /challenge/ be the concatenation of /part_1/, expressed as a + // big-endian unsigned 32-bit integer, /part_2/, expressed as a + // big-endian unsigned 32-bit integer, and the eight bytes of + // /key_3/ in the order they were sent on the wire. + + byte[] part3 = key3.getBytes(); + + byte[] challenge = { + (byte)(part1 >> 24), + (byte)(part1 >> 16), + (byte)(part1 >> 8), + (byte)(part1 >> 0), + (byte)(part2 >> 24), + (byte)(part2 >> 16), + (byte)(part2 >> 8), + (byte)(part2 >> 0), + (byte) part3[0], + (byte) part3[1], + (byte) part3[2], + (byte) part3[3], + (byte) part3[4], + (byte) part3[5], + (byte) part3[6], + (byte) part3[7] + }; + + // 2.6.9. Let /response/ be the MD5 fingerprint of /challenge/ as a big- + // endian 128 bit string. [RFC1321] + + String response = ""; + + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] thedigest = md.digest(challenge); + + response = new String(thedigest,"UTF-8"); + + } catch (NoSuchAlgorithmException e) { + return false; + } + + //2.6.10. - 2.6.13. Send the following ... to the client: + + String responseHandshake = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n" + + "Upgrade: WebSocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Location:" + location + "\r\n" + + "Sec-WebSocket-Origin:" + p.getProperty("origin") + "\r\n"; + if (p.containsKey("sec-websocket-protocol")) { + responseHandshake += "Sec-WebSocket-Protocol: " + p.getProperty("sec-websocket-protocol") + "\r\n"; + } + responseHandshake += "\r\n" + response; // Signifies end of handshake + conn.socketChannel().write(ByteBuffer.wrap(responseHandshake.getBytes(WebSocket.UTF8_CHARSET))); + + return true; + + } + public void onMessage(WebSocket conn, String message) { onClientMessage(conn, message); } From 94b04393919fb77533b9e524df70e57a6cd1e682 Mon Sep 17 00:00:00 2001 From: Nathan Mische Date: Mon, 26 Jul 2010 08:44:57 -0400 Subject: [PATCH 02/12] Big update. Fixed several issues. Server is now configurable for port, origin, subprotocol and draft. Added Draft 76 client. --- example/ChatClient.java | 3 +- example/ChatServer.java | 27 ++- src/WebSocket.java | 112 ++++++---- src/WebSocketClient.java | 304 +++++++++++++++++++++++++-- src/WebSocketHandshake.java | 268 ++++++++++++++++++++++++ src/WebSocketListener.java | 4 +- src/WebSocketProtocol.java | 36 ++++ src/WebSocketServer.java | 401 +++++++++++++++++++++++------------- 8 files changed, 948 insertions(+), 207 deletions(-) create mode 100644 src/WebSocketHandshake.java create mode 100644 src/WebSocketProtocol.java diff --git a/example/ChatClient.java b/example/ChatClient.java index 425084647..16d9326b0 100644 --- a/example/ChatClient.java +++ b/example/ChatClient.java @@ -40,7 +40,8 @@ public void onClose() { * The JFrame for our Chat client. */ private static class Frame extends JFrame implements ActionListener { - private final JTextField uriField; + private static final long serialVersionUID = -997730656792009049L; + private final JTextField uriField; private final JButton connect; private final JTextArea area; private final JTextField chatField; diff --git a/example/ChatServer.java b/example/ChatServer.java index a9571ffee..987cbe12e 100644 --- a/example/ChatServer.java +++ b/example/ChatServer.java @@ -6,8 +6,8 @@ */ public class ChatServer extends WebSocketServer { - public ChatServer(int port) { - super(port); + public ChatServer(int port, String origin, String subprotocol, Draft draft) { + super(port,origin,subprotocol,draft); } public void onClientOpen(WebSocket conn) { @@ -37,13 +37,24 @@ public void onClientMessage(WebSocket conn, String message) { System.out.println(conn + ": " + message); } - public static void main(String[] args) { - int port = 80; - try { - port = Integer.parseInt(args[0]); - } catch(Exception ex) {} - ChatServer s = new ChatServer(port); + public static void main(String[] args) { + + int port = (args.length >= 1) ? Integer.parseInt(args[0]) : 80; + String origin = (args.length >=2) ? args[1] : null; + String subprotocol = (args.length >=3) ? args[2] : null; + Draft draft = (args.length >= 4) ? Draft.valueOf(args[3]) : Draft.AUTO; + + ChatServer s = new ChatServer(port,origin,subprotocol,draft); s.start(); System.out.println("ChatServer started on port: " + s.getPort()); + if (origin != null) { + System.out.println("ChatServer origin: " + s.getOrigin()); + } + if (subprotocol != null) { + System.out.println("ChatServer subprotocol: " + s.getSubProtocol()); + } + if (draft != Draft.AUTO) { + System.out.println("ChatServer draft: " + s.getDraft().toString()); + } } } diff --git a/src/WebSocket.java b/src/WebSocket.java index b8483a3e9..6ab6eb820 100644 --- a/src/WebSocket.java +++ b/src/WebSocket.java @@ -1,9 +1,9 @@ +// TODO: Refactor into proper class hierarchy. import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.NotYetConnectedException; import java.nio.channels.SocketChannel; -import java.nio.charset.Charset; /** * Represents one end (client or server) of a single WebSocket connection. @@ -15,12 +15,8 @@ * by your code. * @author Nathan Rajlich */ -final class WebSocket { - // CONSTANTS /////////////////////////////////////////////////////////////// - /** - * The WebSocket protocol expects UTF-8 encoded bytes. - */ - public static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); +final class WebSocket implements WebSocketProtocol { + // CONSTANTS /////////////////////////////////////////////////////////////// /** * The byte representing CR, or Carriage Return, or \r */ @@ -37,8 +33,7 @@ final class WebSocket { * The byte representing the end of a WebSocket text frame. */ public static final byte END_OF_FRAME = (byte)0xFF; - - + // INSTANCE PROPERTIES ///////////////////////////////////////////////////// /** * The SocketChannel instance to use for this server connection. @@ -61,12 +56,15 @@ final class WebSocket { /** * The bytes that make up the remote handshake. */ - private ByteBuffer remoteHandshake; + private ByteBuffer remoteHandshakeBuffer; /** * The bytes that make up the current text frame being read. */ private ByteBuffer currentFrame; - + /** + * The type of WebSocket. + */ + private ClientServerType wstype; // CONSTRUCTOR ///////////////////////////////////////////////////////////// /** @@ -77,12 +75,13 @@ final class WebSocket { * @param listener The {@link WebSocketListener} to notify of events when * they occur. */ - public WebSocket(SocketChannel socketChannel, WebSocketListener listener) { + public WebSocket(SocketChannel socketChannel, WebSocketListener listener, ClientServerType wstype) { this.socketChannel = socketChannel; this.handshakeComplete = false; - this.remoteHandshake = this.currentFrame = null; + this.remoteHandshakeBuffer = this.currentFrame = null; this.buffer = ByteBuffer.allocate(1); this.wsl = listener; + this.wstype = wstype; } // PUBLIC INSTANCE METHODS ///////////////////////////////////////////////// @@ -169,38 +168,77 @@ private void recieveFrame() { } private void recieveHandshake() throws IOException { - ByteBuffer ch = ByteBuffer.allocate((this.remoteHandshake != null ? this.remoteHandshake.capacity() : 0) + this.buffer.capacity()); - if (this.remoteHandshake != null) { - this.remoteHandshake.rewind(); - ch.put(this.remoteHandshake); + ByteBuffer ch = ByteBuffer.allocate((this.remoteHandshakeBuffer != null ? this.remoteHandshakeBuffer.capacity() : 0) + this.buffer.capacity()); + if (this.remoteHandshakeBuffer != null) { + this.remoteHandshakeBuffer.rewind(); + ch.put(this.remoteHandshakeBuffer); } ch.put(this.buffer); - this.remoteHandshake = ch; + this.remoteHandshakeBuffer = ch; + + WebSocketHandshake handshake; + + byte[] h = this.remoteHandshakeBuffer.array(); + + // Check to see if this is a flash policy request + if (h.length==23 && h[h.length-1] == 0) { + handshake = new WebSocketHandshake(h); + completeHandshake(handshake); + return; + } - // If the ByteBuffer ends with 0x0D 0x0A 0x0D 0x0A - // (or two CRLFs), then the client handshake is complete - // (version 75) + Draft draft; + + String hsString = new String(this.remoteHandshakeBuffer.array(), UTF8_CHARSET); + if (hsString.toLowerCase().contains("\r\nsec-")){ + draft = Draft.DRAFT76; + } else { + draft = Draft.DRAFT75; + } + + if (draft == Draft.DRAFT75 + &&h.length>=4 + && h[h.length-4] == CR + && h[h.length-3] == LF + && h[h.length-2] == CR + && h[h.length-1] == LF) { + + ClientServerType type = (wstype == ClientServerType.CLIENT) ? ClientServerType.SERVER : ClientServerType.CLIENT; + handshake = new WebSocketHandshake(h, type, draft); + completeHandshake(handshake); + return; + + } - // If the ByteBuffer ends with 0x0D 0x0A 0x0D 0x0A - // followed by 8 random bytes, then the client - // handshake is complete (version 76) + if (draft == Draft.DRAFT76 + && wstype == ClientServerType.SERVER + && h.length>=12 + && h[h.length-12] == CR + && h[h.length-11] == LF + && h[h.length-10] == CR + && h[h.length-9] == LF) { + + handshake = new WebSocketHandshake(h, ClientServerType.CLIENT, draft); + completeHandshake(handshake); + return; + + } - byte[] h = this.remoteHandshake.array(); - if ((h.length>=4 && h[h.length-4] == CR - && h[h.length-3] == LF - && h[h.length-2] == CR - && h[h.length-1] == LF) || - (h.length>=8 && h[h.length-12] == CR - && h[h.length-11] == LF - && h[h.length-10] == CR - && h[h.length-9] == LF) || - (h.length==23 && h[h.length-1] == 0)) { - completeHandshake(); + if (draft == Draft.DRAFT76 + && wstype == ClientServerType.CLIENT + && h.length>=20 + && h[h.length-20] == CR + && h[h.length-19] == LF + && h[h.length-18] == CR + && h[h.length-17] == LF) { + + handshake = new WebSocketHandshake(h, ClientServerType.SERVER, draft); + completeHandshake(handshake); + return; } } - private void completeHandshake() throws IOException { - String handshake = new String(this.remoteHandshake.array(), UTF8_CHARSET); + private void completeHandshake(WebSocketHandshake handshake) throws IOException { this.handshakeComplete = true; if (this.wsl.onHandshakeRecieved(this, handshake)) { this.wsl.onOpen(this); diff --git a/src/WebSocketClient.java b/src/WebSocketClient.java index bcfd551ce..b5337e9b0 100644 --- a/src/WebSocketClient.java +++ b/src/WebSocketClient.java @@ -1,3 +1,4 @@ +// TODO: Refactor into proper class hierarchy. import java.io.IOException; import java.net.InetSocketAddress; @@ -6,12 +7,17 @@ import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; import java.util.Iterator; +import java.util.Random; import java.util.Set; + /** * The WebSocketClient is an abstract class that expects a valid - * "ws://" URI to connect to. When connected, an instance recieves important + * "ws://" URI to connect to. When connected, an instance receives important * events related to the life of the connection. A subclass must implement * onOpen, onClose, and onMessage to be * useful. An instance can send messages to it's connected server via the @@ -20,6 +26,23 @@ */ public abstract class WebSocketClient implements Runnable, WebSocketListener { // INSTANCE PROPERTIES ///////////////////////////////////////////////////// + /** + * The challenge for Draft 76 handshake + */ + private byte[] challenge = null; + /** + * The expected challenge respone for Draft 76 handshake + */ + private byte[] expected = null; + /** + * The version of the WebSocket Internet-Draft this client supports. + * Draft 76 by default. + */ + private Draft draft = Draft.DRAFT76; + /** + * The subprotocol this client object supports + */ + private String subprotocol = null; /** * The URI this client is supposed to connect to. */ @@ -38,15 +61,74 @@ public WebSocketClient() {} /** * Constructs a WebSocketClient instance and sets it to the connect to the - * specified URI. The client does not attampt to connect automatically. You + * specified URI. The client does not attempt to connect automatically. You * must call connect first to initiate the socket connection. * @param serverUri */ public WebSocketClient(URI serverUri) { setURI(serverUri); } - + + /** + * Constructs a WebSocketClient instance and sets it to the connect to the + * specified URI using the specified subprotocol. The client does not + * attempt to connect automatically. You must call connect first + * to initiate the socket connection. + * @param serverUri + * @param subprotocol + */ + public WebSocketClient(URI serverUri, String subprotocol) { + setURI(serverUri); + setSubProtocol(subprotocol); + } + + /** + * Constructs a WebSocketClient instance and sets it to the connect to the + * specified URI using the specified subprotocol and draft. The client does not + * attempt to connect automatically. You must call connect first + * to initiate the socket connection. + * @param serverUri + * @param draft DRAFT75 or DRAFT76 + */ + public WebSocketClient(URI serverUri, String subprotocol, String draft) { + setURI(serverUri); + setSubProtocol(subprotocol); + setDraft(Draft.valueOf(draft.toUpperCase())); + } + // PUBLIC INSTANCE METHODS ///////////////////////////////////////////////// + /** + * Sets this WebSocketClient draft. + * @param draft + */ + public void setDraft(Draft draft) { + this.draft = draft; + } + + /** + * Gets the draft that this WebSocketClient supports. + * @return The draft for this WebSocketClient. + */ + public Draft getDraft() { + return draft; + } + + /** + * Sets this WebSocketClient subprotocol. + * @param uri + */ + public void setSubProtocol(String subprotocol) { + this.subprotocol = subprotocol; + } + + /** + * Gets the subprotocol that this WebSocketClient supports. + * @return The subprotocol for this WebSocketClient. + */ + public String getSubProtocol() { + return subprotocol; + } + /** * Sets this WebSocketClient to connect to the specified URI. * @@ -113,7 +195,7 @@ public void run() { Selector selector = Selector.open(); - this.conn = new WebSocket(client, this); + this.conn = new WebSocket(client, this, ClientServerType.CLIENT); client.register(selector, client.validOps()); // Continuous loop that is only supposed to end when close is called @@ -132,18 +214,8 @@ public void run() { if (client.isConnectionPending()) client.finishConnect(); - // Now send WebSocket client-side handshake - String path = "/" + uri.getPath(); - String host = uri.getHost() + (port != 80 ? ":" + port : ""); - String origin = null; // I don't know what to put here!? - String request = "GET "+path+" HTTP/1.1\r\n" + - "Upgrade: WebSocket\r\n" + - "Connection: Upgrade\r\n" + - "Host: "+host+"\r\n" + - "Origin: "+origin+"\r\n" + - //extraHeaders.toString() + - "\r\n"; - conn.socketChannel().write(ByteBuffer.wrap(request.getBytes(WebSocket.UTF8_CHARSET))); + sendClientHandshake(conn); + } // When 'conn' has recieved some data @@ -170,10 +242,157 @@ public void run() { * handshake, false otherwise. * @throws IOException When socket related I/O errors occur. */ - public boolean onHandshakeRecieved(WebSocket conn, String handshake) throws IOException { - // TODO: Do some parsing of the returned handshake, and close connection - // (return false) if we recieved anything unexpected. - return true; + public boolean onHandshakeRecieved(WebSocket conn, WebSocketHandshake handshake) throws IOException { + + if (handshake.getDraft() == Draft.DRAFT75) { + // TODO: Do some parsing of the returned handshake, and close connection + // (return false) if we recieved anything unexpected. + return true; + } else if (handshake.getDraft() == Draft.DRAFT76) { + + System.out.println("Handshake:" + getHexString(handshake.getHandshake())); + + + if (!handshake.getProperty("status-code").equals("101")){ + return false; + } + + if (!handshake.containsKey("upgrade") || !handshake.getProperty("upgrade").equalsIgnoreCase("websocket")) { + return false; + } + + if (!handshake.containsKey("connection") || !handshake.getProperty("connection").equalsIgnoreCase("upgrade")) { + return false; + } + + if (!handshake.containsKey("sec-websocket-origin") || !handshake.getProperty("sec-websocket-origin").equalsIgnoreCase("null")) { + return false; + } + + int port = (uri.getPort() == -1) ? 80 : uri.getPort(); + String location = "ws://"; + location += uri.getHost() + (port != 80 ? ":" + port : ""); + location += "/" + uri.getPath(); + + if (!handshake.containsKey("sec-websocket-location") || !handshake.getProperty("sec-websocket-location").equalsIgnoreCase(location)) { + return false; + } + + if (subprotocol != null){ + // TODO: support lists of protocols + if (!handshake.containsKey("sec-websocket-protocol") || !handshake.getProperty("sec-websocket-protocol").equals(subprotocol)) { + return false; + } + } + + + if (expected == null) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + expected = md.digest(challenge); + } catch (NoSuchAlgorithmException e) { + return false; + } + } + + byte[] reply = handshake.getAsByteArray("key3"); + + if (!Arrays.equals(expected, reply)) { + return false; + } + + return true; + } + + // If we get here, then something must be wrong + return false; + } + + /** + * Builds the handshake for this client. + * @throws IOException + */ + private void sendClientHandshake(WebSocket conn) throws IOException { + + int port = (uri.getPort() == -1) ? 80 : uri.getPort(); + String requestURI = "/" + uri.getPath(); + String host = uri.getHost() + (port != 80 ? ":" + port : ""); + String origin = "null"; + + WebSocketHandshake clientHandshake = new WebSocketHandshake(); + clientHandshake.setType(ClientServerType.CLIENT); + clientHandshake.setDraft(getDraft()); + clientHandshake.put("request-uri", requestURI); + clientHandshake.put("host", host); + clientHandshake.put("origin", origin); + if (subprotocol != null) { + if (getDraft() == Draft.DRAFT75) { + clientHandshake.put("websocket-protocol", subprotocol); + } else { + clientHandshake.put("sec-webSocket-protocol", subprotocol); + } + } + + if (getDraft() == Draft.DRAFT76) { + + Random rand = new Random(); + + int spaces1 = rand.nextInt(11); + int spaces2 = rand.nextInt(11); + + spaces1+=2; + spaces2+=2; + + int max1 = Integer.MAX_VALUE / spaces1; + int max2 = Integer.MAX_VALUE / spaces2; + + int number1 = rand.nextInt(max1+1); + int number2 = rand.nextInt(max2+1); + + Integer product1 = number1 * spaces1; + Integer product2 = number2 * spaces2; + + String key1 = product1.toString(); + String key2 = product2.toString(); + + key1 = addNoise(key1); + key2 = addNoise(key2); + + key1 = addSpaces(key1,spaces1); + key2 = addSpaces(key2,spaces2); + + clientHandshake.put("sec-websocket-key1", key1); + clientHandshake.put("sec-websocket-key2", key2); + + byte[] key3 = new byte[8]; + rand.nextBytes(key3); + + clientHandshake.put("key3", key3); + System.out.println("Key3:" + getHexString(key3)); + + challenge = new byte[] { + (byte)(number1 >> 24), + (byte)(number1 >> 16), + (byte)(number1 >> 8), + (byte)(number1 >> 0), + (byte)(number2 >> 24), + (byte)(number2 >> 16), + (byte)(number2 >> 8), + (byte)(number2 >> 0), + (byte) key3[0], + (byte) key3[1], + (byte) key3[2], + (byte) key3[3], + (byte) key3[4], + (byte) key3[5], + (byte) key3[6], + (byte) key3[7] + }; + + } + + conn.socketChannel().write(ByteBuffer.wrap(clientHandshake.getHandshake())); + } /** @@ -201,9 +420,52 @@ public void onClose(WebSocket conn) { onClose(); } - + private String addNoise (String key) { + + Random rand = new Random(); + + for (int i = 0; i < (rand.nextInt(12) + 1); i++) { + // get a random non-numeric character + int x = 0; + while (x < 33 || ( x >= 48 && x <= 57)) { + x = (rand.nextInt(93) + 33); + } + char r = (char) x; + // get a random position in key + int pos = rand.nextInt(key.length()+1); + key = key.substring(0,pos) + r + key.substring(pos); + } + + return key; + + } + + private String addSpaces (String key, int spaces) { + + Random rand = new Random(); + + for (int i = 0; i < spaces; i++) { + char space = (char) 32; + // get a random position in key that is not + int pos = rand.nextInt(key.length()-1) + 1; + key = key.substring(0,pos) + space + key.substring(pos); + } + + return key; + + } + // ABTRACT METHODS ///////////////////////////////////////////////////////// public abstract void onMessage(String message); public abstract void onOpen(); public abstract void onClose(); + + public static String getHexString(byte[] b) { + String result = ""; + for (int i=0; i < b.length; i++) { + result += " 0x" + + Integer.toString( ( b[i] & 0xff ) + 0x100, 16).substring( 1 ).toUpperCase(); + } + return result; + } } diff --git a/src/WebSocketHandshake.java b/src/WebSocketHandshake.java new file mode 100644 index 000000000..84589ef42 --- /dev/null +++ b/src/WebSocketHandshake.java @@ -0,0 +1,268 @@ +// TODO: Refactor into proper class hierarchy. + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Hashtable; + +public class WebSocketHandshake extends Hashtable implements WebSocketProtocol { + + // INSTANCE PROPERTIES ///////////////////////////////////////////////////// + private static final long serialVersionUID = -280930290679993232L; + /** + * The raw handshake. + */ + private byte[] handshake; + /** + * The WebSocket Internet-Draft version. + */ + private Draft handshakeDraft; + /** + * The type of the handshake. The refers to the source of the handshake, either + * client or server. + */ + private ClientServerType handshakeType; + + // CONSTRUCTOR ///////////////////////////////////////////////////////////// + public WebSocketHandshake() { + super(); + } + + public WebSocketHandshake(byte[] handshake) { + super(); + setHandshake(handshake); + } + + public WebSocketHandshake(byte[] handshake, ClientServerType type, Draft draft) { + super(); + setDraft(draft); + setType(type); + setHandshake(handshake); + } + + // PUBLIC INSTANCE METHODS ///////////////////////////////////////////////// + public void setDraft(Draft handshakeDraft) { + this.handshakeDraft = handshakeDraft; + } + + public Draft getDraft() { + return handshakeDraft; + } + + public void setHandshake(byte[] handshake) { + this.handshake = handshake; + if (handshakeDraft != null && handshakeType != null) { + parseHandshake(); + } + } + + public byte[] getHandshake() { + if (handshake == null) { + buildHandshake(); + } + return handshake; + } + + public void setType(ClientServerType handshakeType) { + this.handshakeType = handshakeType; + } + + public ClientServerType getType() { + return handshakeType; + } + + public String toString() { + return new String(getHandshake(),UTF8_CHARSET); + } + + public String getProperty(String fieldName) { + if (!containsKey(fieldName)) { + return null; + } + return (String) get(fieldName); + } + + public byte[] getAsByteArray(String fieldName) { + if (!containsKey(fieldName)) { + return null; + } + return (byte[]) get(fieldName); + } + + public void parseHandshake() { + + if (this.handshakeDraft == null) throw new NullPointerException("Handshake draft type must be set before parsing."); + if (this.handshakeType == null) throw new NullPointerException("Handshake type type must be set before parsing."); + + // parse the HTTP request-line or HTTP status-line + + String hs = this.toString(); + String[] requestLines = hs.split("\r\n"); + String line = requestLines[0].trim(); + + if (this.handshakeType == ClientServerType.SERVER) { + int sp1 = line.indexOf(" "); + int sp2 = line.indexOf(" ",sp1+1); + String httpVersion = line.substring(0,sp1); + String statusCode = line.substring(sp1+1,sp2); + String reasonPhrase = line.substring(sp2+1,line.length()); + put("http-version", httpVersion); + put("status-code", statusCode); + put("reason-phrase", reasonPhrase); + } + + if (this.handshakeType == ClientServerType.CLIENT) { + int sp1 = line.indexOf(" "); + int sp2 = line.indexOf(" ",sp1+1); + String method = line.substring(0,sp1); + String requestURI = line.substring(sp1+1,sp2); + String httpVersion = line.substring(sp2+1,line.length()); + put("method", method); + put("request-uri", requestURI); + put("http-version", httpVersion); + } + + // parse fields + + for (int i=1; iWebSocket. * @author Nathan Rajlich */ -interface WebSocketListener { +interface WebSocketListener extends WebSocketProtocol { /** * Called when the socket connection is first established, and the WebSocket * handshake has been recieved. This method should parse the @@ -18,7 +18,7 @@ interface WebSocketListener { * should be immediately called afterwards. false if the * handshake was invalid, and the connection should be terminated. */ - public boolean onHandshakeRecieved(WebSocket conn, String handshake) throws IOException; + public boolean onHandshakeRecieved(WebSocket conn, WebSocketHandshake handshake) throws IOException; /** * Called when an entire text frame has been recieved. Do whatever you want * here... diff --git a/src/WebSocketProtocol.java b/src/WebSocketProtocol.java new file mode 100644 index 000000000..4b492ebde --- /dev/null +++ b/src/WebSocketProtocol.java @@ -0,0 +1,36 @@ + +import java.nio.charset.Charset; + +/** + * Constants used by WebSocket protocol. + * @author Nathan Mische + */ +interface WebSocketProtocol { + + /** + * The version of the WebSocket Internet-Draft + */ + public enum Draft { + AUTO, + DRAFT75, + DRAFT76 + } + + /** + * The maximum value of a WebSocket key. + */ + public static final Long MAX_KEY_VALUE = Long.parseLong("4294967295"); + + /** + * The WebSocket protocol expects UTF-8 encoded bytes. + */ + public static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); + + /** + * The type of WebSocket + */ + public enum ClientServerType { + CLIENT, + SERVER + } +} diff --git a/src/WebSocketServer.java b/src/WebSocketServer.java index 8a451494d..37e1f9d70 100644 --- a/src/WebSocketServer.java +++ b/src/WebSocketServer.java @@ -8,10 +8,10 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Iterator; -import java.util.Properties; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; + /** * WebSocketServer is an abstract class that only takes care of the * HTTP handshake portion of WebSockets. It's up to a subclass to add @@ -25,37 +25,54 @@ public abstract class WebSocketServer implements Runnable, WebSocketListener { * the WebSocketServer is binded to. */ public static final int DEFAULT_PORT = 80; + /** + * If a constructor is used that doesn't define the draft, the DEFAULT_DRAFT + * will be the draft of the WebSocket protocl the WebSocketServer supports. + */ + public static final Draft DEFAULT_DRAFT = Draft.AUTO; + /** * The value of handshake when a Flash client requests a policy * file on this server. */ public static final String FLASH_POLICY_REQUEST = "\0"; - - public static final Long MAX_KEY_VALUE = Long.parseLong("4294967295"); - + // INSTANCE PROPERTIES ///////////////////////////////////////////////////// /** * Holds the list of active WebSocket connections. "Active" means WebSocket * handshake is complete and socket can be written to, or read from. */ - private final CopyOnWriteArraySet connections; + private final CopyOnWriteArraySet connections; + /** + * The version of the WebSocket Internet-Draft this client supports. + */ + private Draft draft; + /** + * The origin we will accept connections from. + */ + private String origin; /** * The port number that this WebSocket server should listen on. Default is * 80 (HTTP). */ - private int port; + private int port; /** * The socket channel for this WebSocket server. */ private ServerSocketChannel server; + /** + * The subprotocol that this WebSocket server supports. Default is null. + */ + private String subprotocol; // CONSTRUCTOR ///////////////////////////////////////////////////////////// /** * Nullary constructor. Creates a WebSocketServer that will attempt to - * listen on port DEFAULT_PORT. + * listen on port DEFAULT_PORT and support both draft 75 and 76 of the + * WebSocket protocol. */ public WebSocketServer() { - this(DEFAULT_PORT); + this(DEFAULT_PORT,null,null,DEFAULT_DRAFT); } /** @@ -64,10 +81,132 @@ public WebSocketServer() { * @param port The port number this server should listen on. */ public WebSocketServer(int port) { - this.connections = new CopyOnWriteArraySet(); - setPort(port); + this(port,null,null,DEFAULT_DRAFT); + } + + /** + * Creates a WebSocketServer that will attempt to listen on port + * port using a specified subprotocol. + * @param port The port number this server should listen on. + * @param origin The origin this server supports. + */ + public WebSocketServer(int port, String origin) { + this(port,origin,null,DEFAULT_DRAFT); + } + + /** + * Creates a WebSocketServer that will attempt to listen on port + * port using a specified subprotocol. + * Only allows connections from origin. + * @param port The port number this server should listen on. + * @param origin The origin this server supports. + * @param subprotocol The subprotocol this server supports. + */ + public WebSocketServer(int port, String origin, String subprotocol) { + this(port,origin,subprotocol,DEFAULT_DRAFT); + } + + /** + * Creates a WebSocketServer that will attempt to listen on port + * port using a specified subprotocol. + * Only allows connections from origin using specified + * draft of the WebSocket protocol. + * @param port The port number this server should listen on. + * @param origin The origin this server supports. + * @param subprotocol The subprotocol this server supports. + * @param draft The draft of the WebSocket protocol this server supports. + */ + public WebSocketServer(int port, String origin, String subprotocol, String draft) { + this(port,origin,subprotocol,Draft.valueOf(draft)); + } + + /** + * Creates a WebSocketServer that will attempt to listen on port + * port using a specified subprotocol. + * Only allows connections from origin using specified + * draft of the WebSocket protocol. + * @param port The port number this server should listen on. + * @param origin The origin this server supports. + * @param subprotocol The subprotocol this server supports. + * @param draft The draft of the WebSocket protocol this server supports. + */ + public WebSocketServer(int port, String origin, String subprotocol, Draft draft) { + this.connections = new CopyOnWriteArraySet(); + setPort(port); + setOrigin(origin); + setSubProtocol(subprotocol); + setDraft(draft); + } + + + // PUBLIC INSTANCE METHODS ///////////////////////////////////////////////// + /** + * Sets this WebSocketClient draft. + * @param draft + */ + public void setDraft(Draft draft) { + this.draft = draft; } + /** + * Gets the draft that this WebSocketClient supports. + * @return The draft for this WebSocketClient. + */ + public Draft getDraft() { + return draft; + } + + /** + * Sets the origin that this WebSocketServer should allow connections + * from. + * @param origin The origin to allow connections from. + */ + public void setOrigin(String origin) { + this.origin = origin; + } + + /** + * Gets the origin that this WebSocketServer should allow connections + * from. + * @return The origin. + */ + public String getOrigin() { + return origin; + } + + /** + * Sets the port that this WebSocketServer should listen on. + * @param port The port number to listen on. + */ + public void setPort(int port) { + this.port = port; + } + + /** + * Gets the port number that this server listens on. + * @return The port number. + */ + public int getPort() { + return port; + } + + /** + * Sets this WebSocketClient subprotocol. + * @param uri + */ + public void setSubProtocol(String subprotocol) { + this.subprotocol = subprotocol; + } + + /** + * Gets the subprotocol that this WebSocketClient supports. + * @return The subprotocol for this WebSocketClient. + */ + public String getSubProtocol() { + return subprotocol; + } + + /** * Starts the server thread that binds to the currently set port number and * listeners for WebSocket connection requests. @@ -134,23 +273,7 @@ public WebSocket[] connections() { return this.connections.toArray(new WebSocket[0]); } - /** - * Sets the port that this WebSocketServer should listen on. - * @param port The port number to listen on. - */ - public void setPort(int port) { - this.port = port; - } - - /** - * Gets the port number that this server listens on. - * @return The port number. - */ - public int getPort() { - return port; - } - - + // Runnable IMPLEMENTATION ///////////////////////////////////////////////// public void run() { try { @@ -177,7 +300,7 @@ public void run() { if (key.isAcceptable()) { SocketChannel client = server.accept(); client.configureBlocking(false); - WebSocket c = new WebSocket(client, this); + WebSocket c = new WebSocket(client, this, ClientServerType.SERVER); client.register(selector, SelectionKey.OP_READ, c); } @@ -227,48 +350,37 @@ protected String getFlashSecurityPolicy() { * successfully sent a WebSocket server handshake, false otherwise. * @throws IOException When socket related I/O errors occur. */ - public boolean onHandshakeRecieved(WebSocket conn, String handshake) throws IOException { - if (FLASH_POLICY_REQUEST.equals(handshake)) { + public boolean onHandshakeRecieved(WebSocket conn, WebSocketHandshake handshake) throws IOException { + if (FLASH_POLICY_REQUEST.equals(handshake.toString())) { String policy = getFlashSecurityPolicy() + "\0"; - conn.socketChannel().write(ByteBuffer.wrap(policy.getBytes(WebSocket.UTF8_CHARSET))); + conn.socketChannel().write(ByteBuffer.wrap(policy.getBytes(UTF8_CHARSET))); return false; } - String[] requestLines = handshake.split("\r\n"); - String line; - - Properties p = new Properties(); - for (int i=1; i MAX_KEY_VALUE || key2 > MAX_KEY_VALUE) { return false; @@ -469,8 +581,8 @@ the server could then decide how to respond (or indeed, _whether_ // WebSocket connection. This is a symptom of a cross-protocol // attack. - int spaces1 = p.getProperty("sec-websocket-key1").replaceAll("[^ ]", "").length(); - int spaces2 = p.getProperty("sec-websocket-key2").replaceAll("[^ ]", "").length(); + int spaces1 = handshake.getProperty("sec-websocket-key1").replaceAll("[^ ]", "").length(); + int spaces2 = handshake.getProperty("sec-websocket-key2").replaceAll("[^ ]", "").length(); if (spaces1 == 0 || spaces2 == 0) { return false; @@ -501,7 +613,8 @@ the server could then decide how to respond (or indeed, _whether_ // big-endian unsigned 32-bit integer, and the eight bytes of // /key_3/ in the order they were sent on the wire. - byte[] part3 = key3.getBytes(); + + System.out.println("Key3:" + getHexString(key3)); byte[] challenge = { (byte)(part1 >> 24), @@ -512,44 +625,46 @@ the server could then decide how to respond (or indeed, _whether_ (byte)(part2 >> 16), (byte)(part2 >> 8), (byte)(part2 >> 0), - (byte) part3[0], - (byte) part3[1], - (byte) part3[2], - (byte) part3[3], - (byte) part3[4], - (byte) part3[5], - (byte) part3[6], - (byte) part3[7] + (byte) key3[0], + (byte) key3[1], + (byte) key3[2], + (byte) key3[3], + (byte) key3[4], + (byte) key3[5], + (byte) key3[6], + (byte) key3[7] }; // 2.6.9. Let /response/ be the MD5 fingerprint of /challenge/ as a big- // endian 128 bit string. [RFC1321] - String response = ""; + byte[] response; try { MessageDigest md = MessageDigest.getInstance("MD5"); - byte[] thedigest = md.digest(challenge); - - response = new String(thedigest,"UTF-8"); - + response = md.digest(challenge); } catch (NoSuchAlgorithmException e) { return false; } + System.out.println("Response:"+getHexString(response)); + System.out.println("Response Length:"+response.length); + //2.6.10. - 2.6.13. Send the following ... to the client: - - String responseHandshake = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n" + - "Upgrade: WebSocket\r\n" + - "Connection: Upgrade\r\n" + - "Sec-WebSocket-Location:" + location + "\r\n" + - "Sec-WebSocket-Origin:" + p.getProperty("origin") + "\r\n"; - if (p.containsKey("sec-websocket-protocol")) { - responseHandshake += "Sec-WebSocket-Protocol: " + p.getProperty("sec-websocket-protocol") + "\r\n"; - } - responseHandshake += "\r\n" + response; // Signifies end of handshake - conn.socketChannel().write(ByteBuffer.wrap(responseHandshake.getBytes(WebSocket.UTF8_CHARSET))); + WebSocketHandshake serverHandshake = new WebSocketHandshake(); + serverHandshake.setType(ClientServerType.SERVER); + serverHandshake.setDraft(Draft.DRAFT76); + serverHandshake.put("host", handshake.getProperty("host")); + serverHandshake.put("request-uri", handshake.getProperty("request-uri")); + serverHandshake.put("origin", handshake.getProperty("origin")); + serverHandshake.put("response", response); + if (handshake.containsKey("sec-websocket-protocol")) { + serverHandshake.put("sec-websocket-protocol", handshake.getProperty("sec-websocket-protocol")); + } + + conn.socketChannel().write(ByteBuffer.wrap(serverHandshake.getHandshake())); + return true; } @@ -572,4 +687,14 @@ public void onClose(WebSocket conn) { public abstract void onClientOpen(WebSocket conn); public abstract void onClientClose(WebSocket conn); public abstract void onClientMessage(WebSocket conn, String message); + + public static String getHexString(byte[] b) { + String result = ""; + for (int i=0; i < b.length; i++) { + result += " 0x" + + Integer.toString( ( b[i] & 0xff ) + 0x100, 16).substring( 1 ).toUpperCase(); + } + return result; + } + } From bd08326a1bf56742e4b9aff85770cdbc98af4fcd Mon Sep 17 00:00:00 2001 From: Nathan Mische Date: Mon, 26 Jul 2010 09:22:13 -0400 Subject: [PATCH 03/12] Cleaned up comments. Fixed server handshake validation. --- src/WebSocket.java | 1 + src/WebSocketClient.java | 17 +-- src/WebSocketHandshake.java | 8 +- src/WebSocketListener.java | 2 +- src/WebSocketServer.java | 232 ++++++++---------------------------- 5 files changed, 58 insertions(+), 202 deletions(-) diff --git a/src/WebSocket.java b/src/WebSocket.java index 6ab6eb820..3aae29eb7 100644 --- a/src/WebSocket.java +++ b/src/WebSocket.java @@ -74,6 +74,7 @@ final class WebSocket implements WebSocketProtocol { * with a Selector before construction of this object. * @param listener The {@link WebSocketListener} to notify of events when * they occur. + * @param wstype The type of WebSocket, client or server. */ public WebSocket(SocketChannel socketChannel, WebSocketListener listener, ClientServerType wstype) { this.socketChannel = socketChannel; diff --git a/src/WebSocketClient.java b/src/WebSocketClient.java index b5337e9b0..8d67cc2e3 100644 --- a/src/WebSocketClient.java +++ b/src/WebSocketClient.java @@ -88,6 +88,7 @@ public WebSocketClient(URI serverUri, String subprotocol) { * attempt to connect automatically. You must call connect first * to initiate the socket connection. * @param serverUri + * @param subprotocol * @param draft DRAFT75 or DRAFT76 */ public WebSocketClient(URI serverUri, String subprotocol, String draft) { @@ -250,9 +251,6 @@ public boolean onHandshakeRecieved(WebSocket conn, WebSocketHandshake handshake) return true; } else if (handshake.getDraft() == Draft.DRAFT76) { - System.out.println("Handshake:" + getHexString(handshake.getHandshake())); - - if (!handshake.getProperty("status-code").equals("101")){ return false; } @@ -295,7 +293,7 @@ public boolean onHandshakeRecieved(WebSocket conn, WebSocketHandshake handshake) } } - byte[] reply = handshake.getAsByteArray("key3"); + byte[] reply = handshake.getAsByteArray("response"); if (!Arrays.equals(expected, reply)) { return false; @@ -368,8 +366,7 @@ private void sendClientHandshake(WebSocket conn) throws IOException { rand.nextBytes(key3); clientHandshake.put("key3", key3); - System.out.println("Key3:" + getHexString(key3)); - + challenge = new byte[] { (byte)(number1 >> 24), (byte)(number1 >> 16), @@ -460,12 +457,4 @@ private String addSpaces (String key, int spaces) { public abstract void onOpen(); public abstract void onClose(); - public static String getHexString(byte[] b) { - String result = ""; - for (int i=0; i < b.length; i++) { - result += " 0x" + - Integer.toString( ( b[i] & 0xff ) + 0x100, 16).substring( 1 ).toUpperCase(); - } - return result; - } } diff --git a/src/WebSocketHandshake.java b/src/WebSocketHandshake.java index 84589ef42..dcb71fef7 100644 --- a/src/WebSocketHandshake.java +++ b/src/WebSocketHandshake.java @@ -4,6 +4,10 @@ import java.io.IOException; import java.util.Hashtable; +/** + * The WebSocketHandshake is a class used to create and read client and server handshakes. + * @author Nathan Mishe + */ public class WebSocketHandshake extends Hashtable implements WebSocketProtocol { // INSTANCE PROPERTIES ///////////////////////////////////////////////////// @@ -155,7 +159,7 @@ public void parseHandshake() { } if (this.handshakeType == ClientServerType.SERVER){ //get last 16 bytes - byte[] key3 = { + byte[] response = { getHandshake()[l-16], getHandshake()[l-15], getHandshake()[l-14], @@ -173,7 +177,7 @@ public void parseHandshake() { getHandshake()[l-2], getHandshake()[l-1] }; - put("key3", key3); + put("response", response); } } } diff --git a/src/WebSocketListener.java b/src/WebSocketListener.java index 321adb677..a64f61642 100644 --- a/src/WebSocketListener.java +++ b/src/WebSocketListener.java @@ -13,7 +13,7 @@ interface WebSocketListener extends WebSocketProtocol { * handshake, and return a boolean indicating whether or not the * connection is a valid WebSocket connection. * @param conn The WebSocket instance this event is occuring on. - * @param handshake The entire UTF-8 decoded handshake from the connection. + * @param handshake The WebSocketHandshake. * @return true if the handshake is valid, and onOpen * should be immediately called afterwards. false if the * handshake was invalid, and the connection should be terminated. diff --git a/src/WebSocketServer.java b/src/WebSocketServer.java index 37e1f9d70..42fdcd806 100644 --- a/src/WebSocketServer.java +++ b/src/WebSocketServer.java @@ -16,6 +16,12 @@ * WebSocketServer is an abstract class that only takes care of the * HTTP handshake portion of WebSockets. It's up to a subclass to add * functionality/purpose to the server. + * + * May be configured to listen to listen on a specified + * port using a specified subprotocol. + * May also be configured to only allows connections from a specified origin. + * Can be configure to support a specific draft of the WebSocket protocol + * (DRAFT75 or DRAFT76) or both (AUTO). * @author Nathan Rajlich */ public abstract class WebSocketServer implements Runnable, WebSocketListener { @@ -29,8 +35,7 @@ public abstract class WebSocketServer implements Runnable, WebSocketListener { * If a constructor is used that doesn't define the draft, the DEFAULT_DRAFT * will be the draft of the WebSocket protocl the WebSocketServer supports. */ - public static final Draft DEFAULT_DRAFT = Draft.AUTO; - + public static final Draft DEFAULT_DRAFT = Draft.AUTO; /** * The value of handshake when a Flash client requests a policy * file on this server. @@ -48,7 +53,7 @@ public abstract class WebSocketServer implements Runnable, WebSocketListener { */ private Draft draft; /** - * The origin we will accept connections from. + * The origin this WebSocket server will accept connections from. */ private String origin; /** @@ -86,7 +91,7 @@ public WebSocketServer(int port) { /** * Creates a WebSocketServer that will attempt to listen on port - * port using a specified subprotocol. + * port. Only allows connections from origin. * @param port The port number this server should listen on. * @param origin The origin this server supports. */ @@ -384,6 +389,20 @@ private boolean handleHandshake75(WebSocket conn, WebSocketHandshake handshake) if (prop == null || !prop.equals("Upgrade")) { return false; } + + if (subprotocol != null) { + prop = handshake.getProperty("Websocket-Protocol"); + if (prop == null || !prop.equals(subprotocol)) { + return false; + } + } + + if (origin != null) { + prop = handshake.getProperty("Origin"); + if (prop == null || !prop.startsWith(origin)) { + return false; + } + } // If we've determined that this is a valid WebSocket request, send a // valid WebSocket server handshake, then return true to keep connection alive. @@ -405,195 +424,67 @@ private boolean handleHandshake75(WebSocket conn, WebSocketHandshake handshake) } private boolean handleHandshake76(WebSocket conn, WebSocketHandshake handshake) throws IOException { - - - - // 6.1. Reading the client's opening handshake - - // 6.1.1. The three-character UTF-8 string "GET". - // 6.1.2. A UTF-8-encoded U+0020 SPACE character (0x20 byte). - // 6.1.3. A string consisting of all the bytes up to the next UTF-8-encoded - // U+0020 SPACE character (0x20 byte). The result of decoding this - // string as a UTF-8 string is the name of the resource requested by - // the server. If the server only supports one resource, then this - // can safely be ignored; the client verifies that the right - // resource is supported based on the information included in the - // server's own handshake. The resource name will begin with U+002F - // SOLIDUS character (/) and will only include characters in the - // range U+0021 to U+007E. - - + if (!handshake.getProperty("method").equals("GET") || !handshake.getProperty("request-uri").matches("^/[\u0021-\u007E]*")) { return false; } - // 6.1.4. A string of bytes terminated by a UTF-8-encoded U+000D CARRIAGE - // RETURN U+000A LINE FEED character pair (CRLF). All the - // characters from the second 0x20 byte up to the first 0x0D 0x0A - // byte pair in the data from the client can be safely ignored. (It - // will probably be the string "HTTP/1.1".) - // 6.1.5. A series of fields. - // The expected field names, and the meaning of their corresponding - // values, are as follows. Field names must be compared in an ASCII - // case-insensitive manner. - String prop; - /* - |Upgrade| - Invariant part of the handshake. Will always have a value that is - an ASCII case-insensitive match for the string "WebSocket". - - Can be safely ignored, though the server should abort the - WebSocket connection if this field is absent or has a different - value, to avoid vulnerability to cross-protocol attacks. - */ + prop = handshake.getProperty("upgrade"); if (prop == null || !(prop.equalsIgnoreCase("WebSocket"))) { return false; - } - - /* - |Connection| - Invariant part of the handshake. Will always have a value that is - an ASCII case-insensitive match for the string "Upgrade". - - Can be safely ignored, though the server should abort the - WebSocket connection if this field is absent or has a different - value, to avoid vulnerability to cross-protocol attacks. - */ + } + prop = handshake.getProperty("connection"); if (prop == null || !(prop.equalsIgnoreCase("Upgrade"))) { return false; } - /* - |Host| - The value gives the hostname that the client intended to use when - opening the WebSocket. It would be of interest in particular to - virtual hosting environments, where one server might serve - multiple hosts, and might therefore want to return different data. - - Can be safely ignored, though the server should abort the - WebSocket connection if this field is absent or has a value that - does not match the server's host name, to avoid vulnerability to - cross-protocol attacks and DNS rebinding attacks. - */ if (!handshake.containsKey("host")) { return false; } - /* - |Origin| - The value gives the scheme, hostname, and port (if it's not the - default port for the given scheme) of the page that asked the - client to open the WebSocket. It would be interesting if the - server's operator had deals with operators of other sites, since - the server could then decide how to respond (or indeed, _whether_ - to respond) based on which site was requesting a connection. - [ORIGIN] - - Can be safely ignored, though the server should abort the - WebSocket connection if this field is absent or has a value that - does not match one of the origins the server is expecting to - communicate with, to avoid vulnerability to cross-protocol attacks - and cross-site scripting attacks. - */ + if (!handshake.containsKey("origin")) { return false; - } else if (origin != null && !handshake.getProperty("origin").startsWith(origin)) { - return false; - } + } - /* - |Sec-WebSocket-Protocol| - The value gives the names of subprotocols that the client is - willing to use, as a space-separated list in the order that the - client prefers the protocols. It would be interesting if the - server supports multiple protocols or protocol versions. - - Can be safely ignored, though the server may abort the WebSocket - connection if the field is absent but the conventions for - communicating with the server are such that the field is expected; - and the server should abort the WebSocket connection if the field - does not contain a value that does matches one of the subprotocols - that the server supports, to avoid integrity errors once the - connection is established. - */ - if (handshake.containsKey("sec-websocket-protocol")) { - if (subprotocol != null && !handshake.getProperty("sec-websocket-protocol").equals(subprotocol)) { - return false; - } + if (origin != null) { + prop = handshake.getProperty("origin"); + if (prop == null || !prop.startsWith(origin)) { + return false; + } + } + + if (subprotocol != null) { + prop = handshake.getProperty("sec-websocket-protocol"); + if (prop == null || !prop.equals(subprotocol)) { + return false; + } } - - /* - |Sec-WebSocket-Key1| - - |Sec-WebSocket-Key2| - The values provide the information required for computing the - server's handshake, as described in the next section. - */ + if (!handshake.containsKey("sec-websocket-key1") || !handshake.containsKey("sec-websocket-key2")) { return false; } - - // 6.1.6 After the first 0x0D 0x0A 0x0D 0x0A byte sequence, indicating the - // end of the fields, the client sends eight random bytes. These - // are used in constructing the server handshake. + byte[] key3 = handshake.getAsByteArray("key3"); - - - // 6.2. Sending the server's opening handshake - - // 6.2.3. Let /location/ be the string that results from constructing a - // WebSocket URL from /host/, /port/, /resource name/, and /secure - // flag/ - // String location = "ws://" + handshake.getProperty("host") + handshake.getProperty("request-uri"); - - - // 6.2.4. Let /key-number_1/ be the digits (characters in the range U+0030 - // DIGIT ZERO (0) to U+0039 DIGIT NINE (9)) in /key_1/, interpreted - // as a base ten integer, ignoring all other characters in /key_1/. - - // Let /key-number_2/ be the digits (characters in the range U+0030 - // DIGIT ZERO (0) to U+0039 DIGIT NINE (9)) in /key_2/, interpreted - // as a base ten integer, ignoring all other characters in /key_2/. - - // If either /key-number_1/ or /key-number_2/ is greater than - // 4,294,967,295, then abort the WebSocket connection. This is a - // symptom of an attack. - long key1 = Long.parseLong(handshake.getProperty("sec-websocket-key1").replaceAll("[^0-9]","")); long key2 = Long.parseLong(handshake.getProperty("sec-websocket-key2").replaceAll("[^0-9]","")); if (key1 > MAX_KEY_VALUE || key2 > MAX_KEY_VALUE) { return false; } - - // 6.2.5. Let /spaces_1/ be the number of U+0020 SPACE characters in - // /key_1/. - - // Let /spaces_2/ be the number of U+0020 SPACE characters in - // /key_2/. - - // If either /spaces_1/ or /spaces_2/ is zero, then abort the - // WebSocket connection. This is a symptom of a cross-protocol - // attack. - + int spaces1 = handshake.getProperty("sec-websocket-key1").replaceAll("[^ ]", "").length(); int spaces2 = handshake.getProperty("sec-websocket-key2").replaceAll("[^ ]", "").length(); if (spaces1 == 0 || spaces2 == 0) { return false; } - - // 6.2.6. If /key-number_1/ is not an integral multiple of /spaces_1/, - // then abort the WebSocket connection. - - // If /key-number_2/ is not an integral multiple of /spaces_2/, - // then abort the WebSocket connection. - + long mod1 = key1 % spaces1; long mod2 = key2 % spaces2; @@ -601,20 +492,8 @@ the server could then decide how to respond (or indeed, _whether_ return false; } - // 2.6.7. Let /part_1/ be /key-number_1/ divided by /spaces_1/. - - // Let /part_2/ be /key-number_2/ divided by /spaces_2/. - long part1 = key1/spaces1; long part2 = key2/spaces2; - - // 2.6.8. Let /challenge/ be the concatenation of /part_1/, expressed as a - // big-endian unsigned 32-bit integer, /part_2/, expressed as a - // big-endian unsigned 32-bit integer, and the eight bytes of - // /key_3/ in the order they were sent on the wire. - - - System.out.println("Key3:" + getHexString(key3)); byte[] challenge = { (byte)(part1 >> 24), @@ -635,9 +514,6 @@ the server could then decide how to respond (or indeed, _whether_ (byte) key3[7] }; - // 2.6.9. Let /response/ be the MD5 fingerprint of /challenge/ as a big- - // endian 128 bit string. [RFC1321] - byte[] response; try { @@ -646,12 +522,7 @@ the server could then decide how to respond (or indeed, _whether_ } catch (NoSuchAlgorithmException e) { return false; } - - System.out.println("Response:"+getHexString(response)); - System.out.println("Response Length:"+response.length); - - //2.6.10. - 2.6.13. Send the following ... to the client: - + WebSocketHandshake serverHandshake = new WebSocketHandshake(); serverHandshake.setType(ClientServerType.SERVER); serverHandshake.setDraft(Draft.DRAFT76); @@ -687,14 +558,5 @@ public void onClose(WebSocket conn) { public abstract void onClientOpen(WebSocket conn); public abstract void onClientClose(WebSocket conn); public abstract void onClientMessage(WebSocket conn, String message); - - public static String getHexString(byte[] b) { - String result = ""; - for (int i=0; i < b.length; i++) { - result += " 0x" + - Integer.toString( ( b[i] & 0xff ) + 0x100, 16).substring( 1 ).toUpperCase(); - } - return result; - } - + } From 3779717066dc3c06144811ac8262fd42efbaa2c0 Mon Sep 17 00:00:00 2001 From: Nathan Mische Date: Mon, 26 Jul 2010 09:51:19 -0400 Subject: [PATCH 04/12] More cleanup. --- src/WebSocket.java | 2 +- src/WebSocketClient.java | 2 +- src/WebSocketHandshake.java | 14 +++++++++++++- src/WebSocketServer.java | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/WebSocket.java b/src/WebSocket.java index 3aae29eb7..bdd29146a 100644 --- a/src/WebSocket.java +++ b/src/WebSocket.java @@ -15,7 +15,7 @@ * by your code. * @author Nathan Rajlich */ -final class WebSocket implements WebSocketProtocol { +public final class WebSocket implements WebSocketProtocol { // CONSTANTS /////////////////////////////////////////////////////////////// /** * The byte representing CR, or Carriage Return, or \r diff --git a/src/WebSocketClient.java b/src/WebSocketClient.java index 8d67cc2e3..5a34efb50 100644 --- a/src/WebSocketClient.java +++ b/src/WebSocketClient.java @@ -116,7 +116,7 @@ public Draft getDraft() { /** * Sets this WebSocketClient subprotocol. - * @param uri + * @param subprotocol */ public void setSubProtocol(String subprotocol) { this.subprotocol = subprotocol; diff --git a/src/WebSocketHandshake.java b/src/WebSocketHandshake.java index dcb71fef7..ce33e3bdf 100644 --- a/src/WebSocketHandshake.java +++ b/src/WebSocketHandshake.java @@ -60,7 +60,7 @@ public void setHandshake(byte[] handshake) { } public byte[] getHandshake() { - if (handshake == null) { + if (handshake == null && handshakeDraft != null && handshakeType != null) { buildHandshake(); } return handshake; @@ -92,6 +92,12 @@ public byte[] getAsByteArray(String fieldName) { return (byte[]) get(fieldName); } + /** + * Parses the parts of the handshake into Hashtable keys. Can only be + * called if the handshake draft and type are set. Called automatically + * when handshake is set, if the draft and type are known and set for + * the handshake. + */ public void parseHandshake() { if (this.handshakeDraft == null) throw new NullPointerException("Handshake draft type must be set before parsing."); @@ -182,6 +188,12 @@ public void parseHandshake() { } } + /** + * Generates the handshake byte array based on the handshake draft and type as well as the keys + * set for this handshake. Called automatically + * when handshake is requested, if the draft and type are known and set for + * the handshake. + */ public void buildHandshake() { if (this.handshakeDraft == null) throw new NullPointerException("Handshake draft type must be set before building."); diff --git a/src/WebSocketServer.java b/src/WebSocketServer.java index 42fdcd806..9d76a47d3 100644 --- a/src/WebSocketServer.java +++ b/src/WebSocketServer.java @@ -197,7 +197,7 @@ public int getPort() { /** * Sets this WebSocketClient subprotocol. - * @param uri + * @param subprotocol */ public void setSubProtocol(String subprotocol) { this.subprotocol = subprotocol; From 05d3c44c1317dd872e21ba279860e15deb7c365f Mon Sep 17 00:00:00 2001 From: Nathan Mische Date: Mon, 26 Jul 2010 09:57:54 -0400 Subject: [PATCH 05/12] Updating Javadocs. --- doc/WebSocket.html | 265 ++------ doc/WebSocketClient.html | 259 ++++++-- doc/WebSocketHandshake.html | 572 ++++++++++++++++++ doc/WebSocketListener.html | 86 ++- doc/WebSocketProtocol.ClientServerType.html | 324 ++++++++++ doc/WebSocketProtocol.Draft.html | 340 +++++++++++ doc/WebSocketProtocol.html | 260 ++++++++ doc/WebSocketServer.html | 447 +++++++++++--- doc/allclasses-frame.html | 12 +- doc/allclasses-noframe.html | 12 +- doc/class-use/WebSocket.html | 332 ++++++++++ doc/class-use/WebSocketClient.html | 142 +++++ doc/class-use/WebSocketHandshake.html | 187 ++++++ doc/class-use/WebSocketListener.html | 191 ++++++ .../WebSocketProtocol.ClientServerType.html | 222 +++++++ doc/class-use/WebSocketProtocol.Draft.html | 273 +++++++++ doc/class-use/WebSocketProtocol.html | 207 +++++++ doc/class-use/WebSocketServer.html | 142 +++++ doc/constant-values.html | 12 +- doc/deprecated-list.html | 10 +- doc/help-doc.html | 16 +- doc/index-all.html | 417 ------------- doc/index-files/index-1.html | 144 +++++ doc/index-files/index-10.html | 198 ++++++ doc/index-files/index-11.html | 143 +++++ doc/index-files/index-12.html | 146 +++++ doc/index-files/index-13.html | 203 +++++++ doc/index-files/index-14.html | 143 +++++ doc/index-files/index-15.html | 143 +++++ doc/index-files/index-16.html | 154 +++++ doc/index-files/index-17.html | 192 ++++++ doc/index-files/index-2.html | 158 +++++ doc/index-files/index-3.html | 148 +++++ doc/index-files/index-4.html | 143 +++++ doc/index-files/index-5.html | 144 +++++ doc/index-files/index-6.html | 182 ++++++ doc/index-files/index-7.html | 144 +++++ doc/index-files/index-8.html | 143 +++++ doc/index-files/index-9.html | 143 +++++ doc/index.html | 2 +- doc/overview-tree.html | 31 +- doc/package-frame.html | 23 +- doc/package-summary.html | 36 +- doc/package-tree.html | 31 +- doc/package-use.html | 155 +++++ doc/serialized-form.html | 205 +++++++ 46 files changed, 7046 insertions(+), 836 deletions(-) create mode 100644 doc/WebSocketHandshake.html create mode 100644 doc/WebSocketProtocol.ClientServerType.html create mode 100644 doc/WebSocketProtocol.Draft.html create mode 100644 doc/WebSocketProtocol.html create mode 100644 doc/class-use/WebSocket.html create mode 100644 doc/class-use/WebSocketClient.html create mode 100644 doc/class-use/WebSocketHandshake.html create mode 100644 doc/class-use/WebSocketListener.html create mode 100644 doc/class-use/WebSocketProtocol.ClientServerType.html create mode 100644 doc/class-use/WebSocketProtocol.Draft.html create mode 100644 doc/class-use/WebSocketProtocol.html create mode 100644 doc/class-use/WebSocketServer.html delete mode 100644 doc/index-all.html create mode 100644 doc/index-files/index-1.html create mode 100644 doc/index-files/index-10.html create mode 100644 doc/index-files/index-11.html create mode 100644 doc/index-files/index-12.html create mode 100644 doc/index-files/index-13.html create mode 100644 doc/index-files/index-14.html create mode 100644 doc/index-files/index-15.html create mode 100644 doc/index-files/index-16.html create mode 100644 doc/index-files/index-17.html create mode 100644 doc/index-files/index-2.html create mode 100644 doc/index-files/index-3.html create mode 100644 doc/index-files/index-4.html create mode 100644 doc/index-files/index-5.html create mode 100644 doc/index-files/index-6.html create mode 100644 doc/index-files/index-7.html create mode 100644 doc/index-files/index-8.html create mode 100644 doc/index-files/index-9.html create mode 100644 doc/package-use.html create mode 100644 doc/serialized-form.html diff --git a/doc/WebSocket.html b/doc/WebSocket.html index 801d5c8a2..15fe3b7e0 100644 --- a/doc/WebSocket.html +++ b/doc/WebSocket.html @@ -2,12 +2,12 @@ - + WebSocket - + @@ -39,9 +39,10 @@ Package   Class  + Use  Tree  Deprecated  - Index  + Index  Help  @@ -90,9 +91,12 @@

java.lang.Object extended by WebSocket +
+
All Implemented Interfaces:
WebSocketProtocol
+

-
final class WebSocket
extends java.lang.Object
+
public final class WebSocket
extends java.lang.Object
implements WebSocketProtocol

@@ -106,24 +110,39 @@

+

+
Author:
+
Nathan Rajlich
+

- + - + +Nested Class Summary + +
-Field Summary
+ + + - - + +
Nested classes/interfaces inherited from interface WebSocketProtocol
-private  java.nio.ByteBufferbuffer +WebSocketProtocol.ClientServerType, WebSocketProtocol.Draft
+  + -
-          The 1-byte buffer reused throughout the WebSocket connection to read data. + + + + - - - - - - - - - - - - - - - - - - - +
+Field Summary
@@ -135,14 +154,6 @@

-private  java.nio.ByteBuffercurrentFrame - -
-          The bytes that make up the current text frame being read.
static byte END_OF_FRAME @@ -151,15 +162,6 @@

-private  booleanhandshakeComplete - -
-          Internally used to determine whether to recieve data as part of the - remote handshake, or as part of a text frame.
static byte LF @@ -168,43 +170,20 @@

-private  java.nio.ByteBufferremoteHandshake - -
-          The bytes that make up the remote handshake.
-private  java.nio.channels.SocketChannelsocketChannel - -
-          The SocketChannel instance to use for this server connection.
static byte START_OF_FRAME
          The byte representing the beginning of a WebSocket text frame.
-static java.nio.charset.CharsetUTF8_CHARSET - -
-          The WebSocket protocol expects UTF-8 encoded bytes.
+ + + - - +
Fields inherited from interface WebSocketProtocol
-private  WebSocketListenerwsl - -
-          The listener to notify of WebSocket events.
MAX_KEY_VALUE, UTF8_CHARSET
  @@ -217,8 +196,9 @@

Constructor Summary -WebSocket(java.nio.channels.SocketChannel socketChannel, - WebSocketListener listener) +WebSocket(java.nio.channels.SocketChannel socketChannel, + WebSocketListener listener, + WebSocketProtocol.ClientServerType wstype)
          Used in WebSocketServer and WebSocketClient. @@ -244,14 +224,6 @@

-private  void -completeHandshake() - -
-            - - -  void handleRead() @@ -261,22 +233,6 @@

-private  void -recieveFrame() - -
-            - - - -private  void -recieveHandshake() - -
-            - - -  void send(java.lang.String text) @@ -314,18 +270,6 @@

-

-UTF8_CHARSET

-
-public static final java.nio.charset.Charset UTF8_CHARSET
-
-
The WebSocket protocol expects UTF-8 encoded bytes. -

-

-
-
-
-

CR

@@ -372,80 +316,6 @@ 

See Also:
Constant Field Values
-
- -

-socketChannel

-
-private final java.nio.channels.SocketChannel socketChannel
-
-
The SocketChannel instance to use for this server connection. - This is used to read and write data to. -

-

-
-
-
- -

-handshakeComplete

-
-private boolean handshakeComplete
-
-
Internally used to determine whether to recieve data as part of the - remote handshake, or as part of a text frame. -

-

-
-
-
- -

-wsl

-
-private WebSocketListener wsl
-
-
The listener to notify of WebSocket events. -

-

-
-
-
- -

-buffer

-
-private java.nio.ByteBuffer buffer
-
-
The 1-byte buffer reused throughout the WebSocket connection to read data. -

-

-
-
-
- -

-remoteHandshake

-
-private java.nio.ByteBuffer remoteHandshake
-
-
The bytes that make up the remote handshake. -

-

-
-
-
- -

-currentFrame

-
-private java.nio.ByteBuffer currentFrame
-
-
The bytes that make up the current text frame being read. -

-

-
-
@@ -457,11 +327,12 @@

-

+

WebSocket

 public WebSocket(java.nio.channels.SocketChannel socketChannel,
-                 WebSocketListener listener)
+ WebSocketListener listener, + WebSocketProtocol.ClientServerType wstype)
Used in WebSocketServer and WebSocketClient.

@@ -469,7 +340,7 @@

Parameters:
socketChannel - The SocketChannel instance to read and write to. The channel should already be registered with a Selector before construction of this object.
listener - The WebSocketListener to notify of events when - they occur.

+ they occur.
wstype - The type of WebSocket, client or server. @@ -492,6 +363,9 @@

WebSocket's SocketChannel connection.

+
+
+
Throws:
java.io.IOException - When socket related I/O errors occur.
@@ -509,6 +383,9 @@

event handler.

+
+
+
Throws:
java.io.IOException - When socket related I/O errors occur.
@@ -523,6 +400,9 @@

throws java.io.IOException
+
+
+
Throws:
java.io.IOException
@@ -538,44 +418,8 @@

-

-
- -

-recieveFrame

-
-private void recieveFrame()
-
-
-
-
-
-
- -

-recieveHandshake

-
-private void recieveHandshake()
-                       throws java.io.IOException
-
- -
Throws: -
java.io.IOException
-
-
- -

-completeHandshake

-
-private void completeHandshake()
-                        throws java.io.IOException
-
-
- -
Throws: -
java.io.IOException
@@ -593,9 +437,10 @@

Package   Class  + Use  Tree  Deprecated  - Index  + Index  Help  diff --git a/doc/WebSocketClient.html b/doc/WebSocketClient.html index b96133968..540c57473 100644 --- a/doc/WebSocketClient.html +++ b/doc/WebSocketClient.html @@ -2,12 +2,12 @@ - + WebSocketClient - + @@ -39,9 +39,10 @@ Package   Class  + Use  Tree  Deprecated  - Index  + Index  Help  @@ -54,7 +55,7 @@  PREV CLASS  - NEXT CLASSNEXT CLASS FRAMES    NO FRAMES   @@ -74,9 +75,9 @@ - SUMMARY: NESTED | FIELD | CONSTR | METHOD + SUMMARY: NESTED | FIELD | CONSTR | METHOD -DETAIL: FIELD | CONSTR | METHOD +DETAIL: FIELD | CONSTR | METHOD @@ -91,7 +92,7 @@

extended by WebSocketClient
-
All Implemented Interfaces:
java.lang.Runnable, WebSocketListener
+
All Implemented Interfaces:
java.lang.Runnable, WebSocketListener, WebSocketProtocol

@@ -100,7 +101,7 @@

The WebSocketClient is an abstract class that expects a valid - "ws://" URI to connect to. When connected, an instance recieves important + "ws://" URI to connect to. When connected, an instance receives important events related to the life of the connection. A subclass must implement onOpen, onClose, and onMessage to be useful. An instance can send messages to it's connected server via the @@ -108,9 +109,32 @@

+

+
Author:
+
Nathan Rajlich
+

+ + + + + + + +
+Nested Class Summary
+ + + + + + + +
Nested classes/interfaces inherited from interface WebSocketProtocol
WebSocketProtocol.ClientServerType, WebSocketProtocol.Draft
@@ -119,21 +143,14 @@

Field Summary - - -private  WebSocket -conn - -
-          The WebSocket instance this client object wraps. + +  + + + - - +
Fields inherited from interface WebSocketProtocol
-private  java.net.URIuri - -
-          The URI this client is supposed to connect to.
MAX_KEY_VALUE, UTF8_CHARSET
  @@ -158,6 +175,23 @@

          Constructs a WebSocketClient instance and sets it to the connect to the specified URI. + +WebSocketClient(java.net.URI serverUri, + java.lang.String subprotocol) + +
+          Constructs a WebSocketClient instance and sets it to the connect to the + specified URI using the specified subprotocol. + + +WebSocketClient(java.net.URI serverUri, + java.lang.String subprotocol, + java.lang.String draft) + +
+          Constructs a WebSocketClient instance and sets it to the connect to the + specified URI using the specified subprotocol and draft. +   @@ -188,6 +222,22 @@

+ WebSocketProtocol.Draft +getDraft() + +
+          Gets the draft that this WebSocketClient supports. + + + + java.lang.String +getSubProtocol() + +
+          Gets the subprotocol that this WebSocketClient supports. + + +  java.net.URI getURI() @@ -214,8 +264,8 @@

 boolean -onHandshakeRecieved(WebSocket conn, - java.lang.String handshake) +onHandshakeRecieved(WebSocket conn, + WebSocketHandshake handshake)
          Parses the server's handshake to verify that it's a valid WebSocket @@ -273,6 +323,22 @@

 void +setDraft(WebSocketProtocol.Draft draft) + +
+          Sets this WebSocketClient draft. + + + + void +setSubProtocol(java.lang.String subprotocol) + +
+          Sets this WebSocketClient subprotocol. + + + + void setURI(java.net.URI uri)
@@ -291,71 +357,71 @@

 

- + - + +Constructor Detail
-Field Detail
-

-uri

+

+WebSocketClient

-private java.net.URI uri
+public WebSocketClient()
-
The URI this client is supposed to connect to. +
Nullary constructor. You must call setURI before calling + connect, otherwise an exception will be thrown.

-

-

-

-conn

+

+WebSocketClient

-private WebSocket conn
+public WebSocketClient(java.net.URI serverUri)
-
The WebSocket instance this client object wraps. +
Constructs a WebSocketClient instance and sets it to the connect to the + specified URI. The client does not attempt to connect automatically. You + must call connect first to initiate the socket connection.

+
Parameters:
serverUri -
-
- - - - - - - - -
-Constructor Detail
+
-

+

WebSocketClient

-public WebSocketClient()
+public WebSocketClient(java.net.URI serverUri, + java.lang.String subprotocol)
-
Nullary constructor. You must call setURI before calling - connect, otherwise an exception will be thrown. +
Constructs a WebSocketClient instance and sets it to the connect to the + specified URI using the specified subprotocol. The client does not + attempt to connect automatically. You must call connect first + to initiate the socket connection.

+

+
Parameters:
serverUri -
subprotocol -

-

+

WebSocketClient

-public WebSocketClient(java.net.URI serverUri)
+public WebSocketClient(java.net.URI serverUri, + java.lang.String subprotocol, + java.lang.String draft)
Constructs a WebSocketClient instance and sets it to the connect to the - specified URI. The client does not attampt to connect automatically. You - must call connect first to initiate the socket connection. + specified URI using the specified subprotocol and draft. The client does not + attempt to connect automatically. You must call connect first + to initiate the socket connection.

-
Parameters:
serverUri -
+
Parameters:
serverUri -
subprotocol -
draft - DRAFT75 or DRAFT76
@@ -368,6 +434,72 @@

+

+setDraft

+
+public void setDraft(WebSocketProtocol.Draft draft)
+
+
Sets this WebSocketClient draft. +

+

+
+
+
+
Parameters:
draft -
+
+
+
+ +

+getDraft

+
+public WebSocketProtocol.Draft getDraft()
+
+
Gets the draft that this WebSocketClient supports. +

+

+
+
+
+ +
Returns:
The draft for this WebSocketClient.
+
+
+
+ +

+setSubProtocol

+
+public void setSubProtocol(java.lang.String subprotocol)
+
+
Sets this WebSocketClient subprotocol. +

+

+
+
+
+
Parameters:
subprotocol -
+
+
+
+ +

+getSubProtocol

+
+public java.lang.String getSubProtocol()
+
+
Gets the subprotocol that this WebSocketClient supports. +

+

+
+
+
+ +
Returns:
The subprotocol for this WebSocketClient.
+
+
+
+

setURI

@@ -476,18 +608,18 @@ 


-

+

onHandshakeRecieved

 public boolean onHandshakeRecieved(WebSocket conn,
-                                   java.lang.String handshake)
+                                   WebSocketHandshake handshake)
                             throws java.io.IOException
Parses the server's handshake to verify that it's a valid WebSocket handshake.

-
Specified by:
onHandshakeRecieved in interface WebSocketListener
+
Specified by:
onHandshakeRecieved in interface WebSocketListener
Parameters:
conn - The WebSocket instance who's handshake has been recieved. @@ -604,9 +736,10 @@

Package   Class  + Use  Tree  Deprecated  - Index  + Index  Help  @@ -619,7 +752,7 @@

 PREV CLASS  - NEXT CLASSNEXT CLASS FRAMES    NO FRAMES   @@ -639,9 +772,9 @@

- SUMMARY: NESTED | FIELD | CONSTR | METHOD + SUMMARY: NESTED | FIELD | CONSTR | METHOD -DETAIL: FIELD | CONSTR | METHOD +DETAIL: FIELD | CONSTR | METHOD diff --git a/doc/WebSocketHandshake.html b/doc/WebSocketHandshake.html new file mode 100644 index 000000000..4b539a87a --- /dev/null +++ b/doc/WebSocketHandshake.html @@ -0,0 +1,572 @@ + + + + + + +WebSocketHandshake + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + +
+ +

+Class WebSocketHandshake

+
+java.lang.Object
+  extended by java.util.Dictionary<K,V>
+      extended by java.util.Hashtable<java.lang.String,java.lang.Object>
+          extended by WebSocketHandshake
+
+
+
All Implemented Interfaces:
java.io.Serializable, java.lang.Cloneable, java.util.Map<java.lang.String,java.lang.Object>, WebSocketProtocol
+
+
+
+
public class WebSocketHandshake
extends java.util.Hashtable<java.lang.String,java.lang.Object>
implements WebSocketProtocol
+ + +

+The WebSocketHandshake is a class used to create and read client and server handshakes. +

+ +

+

+
Author:
+
Nathan Mishe
+
See Also:
Serialized Form
+
+ +

+ + + + + + + +
+Nested Class Summary
+ + + + + + + +
Nested classes/interfaces inherited from interface WebSocketProtocol
WebSocketProtocol.ClientServerType, WebSocketProtocol.Draft
+  + + + + + + + +
+Field Summary
+ + + + + + + +
Fields inherited from interface WebSocketProtocol
MAX_KEY_VALUE, UTF8_CHARSET
+  + + + + + + + + + + + + + + + + +
+Constructor Summary
WebSocketHandshake() + +
+           
WebSocketHandshake(byte[] handshake) + +
+           
WebSocketHandshake(byte[] handshake, + WebSocketProtocol.ClientServerType type, + WebSocketProtocol.Draft draft) + +
+           
+  + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+Method Summary
+ voidbuildHandshake() + +
+          Generates the handshake byte array based on the handshake draft and type as well as the keys + set for this handshake.
+ byte[]getAsByteArray(java.lang.String fieldName) + +
+           
+ WebSocketProtocol.DraftgetDraft() + +
+           
+ byte[]getHandshake() + +
+           
+ java.lang.StringgetProperty(java.lang.String fieldName) + +
+           
+ WebSocketProtocol.ClientServerTypegetType() + +
+           
+ voidparseHandshake() + +
+          Parses the parts of the handshake into Hashtable keys.
+ voidsetDraft(WebSocketProtocol.Draft handshakeDraft) + +
+           
+ voidsetHandshake(byte[] handshake) + +
+           
+ voidsetType(WebSocketProtocol.ClientServerType handshakeType) + +
+           
+ java.lang.StringtoString() + +
+           
+ + + + + + + +
Methods inherited from class java.util.Hashtable
clear, clone, contains, containsKey, containsValue, elements, entrySet, equals, get, hashCode, isEmpty, keys, keySet, put, putAll, rehash, remove, size, values
+ + + + + + + +
Methods inherited from class java.lang.Object
finalize, getClass, notify, notifyAll, wait, wait, wait
+  +

+ + + + + + + + +
+Constructor Detail
+ +

+WebSocketHandshake

+
+public WebSocketHandshake()
+
+
+
+ +

+WebSocketHandshake

+
+public WebSocketHandshake(byte[] handshake)
+
+
+
+ +

+WebSocketHandshake

+
+public WebSocketHandshake(byte[] handshake,
+                          WebSocketProtocol.ClientServerType type,
+                          WebSocketProtocol.Draft draft)
+
+
+ + + + + + + + +
+Method Detail
+ +

+setDraft

+
+public void setDraft(WebSocketProtocol.Draft handshakeDraft)
+
+
+
+
+
+
+
+
+
+ +

+getDraft

+
+public WebSocketProtocol.Draft getDraft()
+
+
+
+
+
+
+
+
+
+ +

+setHandshake

+
+public void setHandshake(byte[] handshake)
+
+
+
+
+
+
+
+
+
+ +

+getHandshake

+
+public byte[] getHandshake()
+
+
+
+
+
+
+
+
+
+ +

+setType

+
+public void setType(WebSocketProtocol.ClientServerType handshakeType)
+
+
+
+
+
+
+
+
+
+ +

+getType

+
+public WebSocketProtocol.ClientServerType getType()
+
+
+
+
+
+
+
+
+
+ +

+toString

+
+public java.lang.String toString()
+
+
+
Overrides:
toString in class java.util.Hashtable<java.lang.String,java.lang.Object>
+
+
+
+
+
+
+ +

+getProperty

+
+public java.lang.String getProperty(java.lang.String fieldName)
+
+
+
+
+
+
+
+
+
+ +

+getAsByteArray

+
+public byte[] getAsByteArray(java.lang.String fieldName)
+
+
+
+
+
+
+
+
+
+ +

+parseHandshake

+
+public void parseHandshake()
+
+
Parses the parts of the handshake into Hashtable keys. Can only be + called if the handshake draft and type are set. Called automatically + when handshake is set, if the draft and type are known and set for + the handshake. +

+

+
+
+
+
+
+
+
+ +

+buildHandshake

+
+public void buildHandshake()
+
+
Generates the handshake byte array based on the handshake draft and type as well as the keys + set for this handshake. Called automatically + when handshake is requested, if the draft and type are known and set for + the handshake. +

+

+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + +
+ + + diff --git a/doc/WebSocketListener.html b/doc/WebSocketListener.html index c48952f35..98b48d11a 100644 --- a/doc/WebSocketListener.html +++ b/doc/WebSocketListener.html @@ -2,12 +2,12 @@ - + WebSocketListener - + @@ -39,9 +39,10 @@ Package   Class  + Use  Tree  Deprecated  - Index  + Index  Help  @@ -53,8 +54,8 @@ PREV CLASS  - NEXT CLASSPREV CLASS  + NEXT CLASS FRAMES    NO FRAMES   @@ -87,11 +88,14 @@

Interface WebSocketListener

+
All Superinterfaces:
WebSocketProtocol
+
+
All Known Implementing Classes:
WebSocketClient, WebSocketServer

-
interface WebSocketListener
+
interface WebSocketListener
extends WebSocketProtocol

@@ -100,10 +104,51 @@

+

+
Author:
+
Nathan Rajlich
+

+ + + + + + + +
+Nested Class Summary
+ + + + + + + +
Nested classes/interfaces inherited from interface WebSocketProtocol
WebSocketProtocol.ClientServerType, WebSocketProtocol.Draft
+  + + + + + + +
+Field Summary
+ + + + + + + +
Fields inherited from interface WebSocketProtocol
MAX_KEY_VALUE, UTF8_CHARSET
@@ -124,8 +169,8 @@

 boolean -onHandshakeRecieved(WebSocket conn, - java.lang.String handshake) +onHandshakeRecieved(WebSocket conn, + WebSocketHandshake handshake)
          Called when the socket connection is first established, and the WebSocket @@ -162,11 +207,11 @@

-

+

onHandshakeRecieved

 boolean onHandshakeRecieved(WebSocket conn,
-                            java.lang.String handshake)
+                            WebSocketHandshake handshake)
                             throws java.io.IOException
Called when the socket connection is first established, and the WebSocket @@ -175,7 +220,10 @@

connection is a valid WebSocket connection.

-
Parameters:
conn - The WebSocket instance this event is occuring on.
handshake - The entire UTF-8 decoded handshake from the connection. +
+
+
+
Parameters:
conn - The WebSocket instance this event is occuring on.
handshake - The WebSocketHandshake.
Returns:
true if the handshake is valid, and onOpen should be immediately called afterwards. false if the handshake was invalid, and the connection should be terminated. @@ -195,6 +243,9 @@

here...

+
+
+
Parameters:
conn - The WebSocket instance this event is occuring on.
message - The UTF-8 decoded message that was recieved.

@@ -210,6 +261,9 @@

and we are ready to send/recieve data.

+
+
+
Parameters:
conn - The WebSocket instance this event is occuring on.

@@ -224,6 +278,9 @@

other end of the WebSocket connection is closed.

+
+
+
Parameters:
conn - The WebSocket instance this event is occuring on.
@@ -242,9 +299,10 @@

Package   Class  + Use  Tree  Deprecated  - Index  + Index  Help  @@ -256,8 +314,8 @@

PREV CLASS  - NEXT CLASSPREV CLASS  + NEXT CLASS FRAMES    NO FRAMES   diff --git a/doc/WebSocketProtocol.ClientServerType.html b/doc/WebSocketProtocol.ClientServerType.html new file mode 100644 index 000000000..b93d8f930 --- /dev/null +++ b/doc/WebSocketProtocol.ClientServerType.html @@ -0,0 +1,324 @@ + + + + + + +WebSocketProtocol.ClientServerType + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + +
+ +

+Enum WebSocketProtocol.ClientServerType

+
+java.lang.Object
+  extended by java.lang.Enum<WebSocketProtocol.ClientServerType>
+      extended by WebSocketProtocol.ClientServerType
+
+
+
All Implemented Interfaces:
java.io.Serializable, java.lang.Comparable<WebSocketProtocol.ClientServerType>
+
+
+
Enclosing interface:
WebSocketProtocol
+
+
+
+
public static enum WebSocketProtocol.ClientServerType
extends java.lang.Enum<WebSocketProtocol.ClientServerType>
+ + +

+The type of WebSocket +

+ +

+


+ +

+ + + + + + + + + + + + + +
+Enum Constant Summary
CLIENT + +
+           
SERVER + +
+           
+  + + + + + + + + + + + + + + + +
+Method Summary
+static WebSocketProtocol.ClientServerTypevalueOf(java.lang.String name) + +
+          Returns the enum constant of this type with the specified name.
+static WebSocketProtocol.ClientServerType[]values() + +
+          Returns an array containing the constants of this enum type, in +the order they are declared.
+ + + + + + + +
Methods inherited from class java.lang.Enum
clone, compareTo, equals, finalize, getDeclaringClass, hashCode, name, ordinal, toString, valueOf
+ + + + + + + +
Methods inherited from class java.lang.Object
getClass, notify, notifyAll, wait, wait, wait
+  +

+ + + + + + + + +
+Enum Constant Detail
+ +

+CLIENT

+
+public static final WebSocketProtocol.ClientServerType CLIENT
+
+
+
+
+
+ +

+SERVER

+
+public static final WebSocketProtocol.ClientServerType SERVER
+
+
+
+
+ + + + + + + + +
+Method Detail
+ +

+values

+
+public static WebSocketProtocol.ClientServerType[] values()
+
+
Returns an array containing the constants of this enum type, in +the order they are declared. This method may be used to iterate +over the constants as follows: +
+for (WebSocketProtocol.ClientServerType c : WebSocketProtocol.ClientServerType.values())
+    System.out.println(c);
+
+

+

+ +
Returns:
an array containing the constants of this enum type, in +the order they are declared
+
+
+
+ +

+valueOf

+
+public static WebSocketProtocol.ClientServerType valueOf(java.lang.String name)
+
+
Returns the enum constant of this type with the specified name. +The string must match exactly an identifier used to declare an +enum constant in this type. (Extraneous whitespace characters are +not permitted.) +

+

+
Parameters:
name - the name of the enum constant to be returned. +
Returns:
the enum constant with the specified name +
Throws: +
java.lang.IllegalArgumentException - if this enum type has no constant +with the specified name +
java.lang.NullPointerException - if the argument is null
+
+
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + +
+ + + diff --git a/doc/WebSocketProtocol.Draft.html b/doc/WebSocketProtocol.Draft.html new file mode 100644 index 000000000..868e3839b --- /dev/null +++ b/doc/WebSocketProtocol.Draft.html @@ -0,0 +1,340 @@ + + + + + + +WebSocketProtocol.Draft + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + +
+ +

+Enum WebSocketProtocol.Draft

+
+java.lang.Object
+  extended by java.lang.Enum<WebSocketProtocol.Draft>
+      extended by WebSocketProtocol.Draft
+
+
+
All Implemented Interfaces:
java.io.Serializable, java.lang.Comparable<WebSocketProtocol.Draft>
+
+
+
Enclosing interface:
WebSocketProtocol
+
+
+
+
public static enum WebSocketProtocol.Draft
extends java.lang.Enum<WebSocketProtocol.Draft>
+ + +

+The version of the WebSocket Internet-Draft +

+ +

+


+ +

+ + + + + + + + + + + + + + + + +
+Enum Constant Summary
AUTO + +
+           
DRAFT75 + +
+           
DRAFT76 + +
+           
+  + + + + + + + + + + + + + + + +
+Method Summary
+static WebSocketProtocol.DraftvalueOf(java.lang.String name) + +
+          Returns the enum constant of this type with the specified name.
+static WebSocketProtocol.Draft[]values() + +
+          Returns an array containing the constants of this enum type, in +the order they are declared.
+ + + + + + + +
Methods inherited from class java.lang.Enum
clone, compareTo, equals, finalize, getDeclaringClass, hashCode, name, ordinal, toString, valueOf
+ + + + + + + +
Methods inherited from class java.lang.Object
getClass, notify, notifyAll, wait, wait, wait
+  +

+ + + + + + + + +
+Enum Constant Detail
+ +

+AUTO

+
+public static final WebSocketProtocol.Draft AUTO
+
+
+
+
+
+ +

+DRAFT75

+
+public static final WebSocketProtocol.Draft DRAFT75
+
+
+
+
+
+ +

+DRAFT76

+
+public static final WebSocketProtocol.Draft DRAFT76
+
+
+
+
+ + + + + + + + +
+Method Detail
+ +

+values

+
+public static WebSocketProtocol.Draft[] values()
+
+
Returns an array containing the constants of this enum type, in +the order they are declared. This method may be used to iterate +over the constants as follows: +
+for (WebSocketProtocol.Draft c : WebSocketProtocol.Draft.values())
+    System.out.println(c);
+
+

+

+ +
Returns:
an array containing the constants of this enum type, in +the order they are declared
+
+
+
+ +

+valueOf

+
+public static WebSocketProtocol.Draft valueOf(java.lang.String name)
+
+
Returns the enum constant of this type with the specified name. +The string must match exactly an identifier used to declare an +enum constant in this type. (Extraneous whitespace characters are +not permitted.) +

+

+
Parameters:
name - the name of the enum constant to be returned. +
Returns:
the enum constant with the specified name +
Throws: +
java.lang.IllegalArgumentException - if this enum type has no constant +with the specified name +
java.lang.NullPointerException - if the argument is null
+
+
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + +
+ + + diff --git a/doc/WebSocketProtocol.html b/doc/WebSocketProtocol.html new file mode 100644 index 000000000..44835d20b --- /dev/null +++ b/doc/WebSocketProtocol.html @@ -0,0 +1,260 @@ + + + + + + +WebSocketProtocol + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + +
+ +

+Interface WebSocketProtocol

+
+
All Known Subinterfaces:
WebSocketListener
+
+
+
All Known Implementing Classes:
WebSocket, WebSocketClient, WebSocketHandshake, WebSocketServer
+
+
+
+
interface WebSocketProtocol
+ + +

+Constants used by WebSocket protocol. +

+ +

+

+
Author:
+
Nathan Mische
+
+
+ +

+ + + + + + + + + + + + + + + +
+Nested Class Summary
+static classWebSocketProtocol.ClientServerType + +
+          The type of WebSocket
+static classWebSocketProtocol.Draft + +
+          The version of the WebSocket Internet-Draft
+ + + + + + + + + + + + + + +
+Field Summary
+static java.lang.LongMAX_KEY_VALUE + +
+          The maximum value of a WebSocket key.
+static java.nio.charset.CharsetUTF8_CHARSET + +
+          The WebSocket protocol expects UTF-8 encoded bytes.
+  +

+ + + + + + + + +
+Field Detail
+ +

+MAX_KEY_VALUE

+
+static final java.lang.Long MAX_KEY_VALUE
+
+
The maximum value of a WebSocket key. +

+

+
+
+
+ +

+UTF8_CHARSET

+
+static final java.nio.charset.Charset UTF8_CHARSET
+
+
The WebSocket protocol expects UTF-8 encoded bytes. +

+

+
+
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + +
+ + + diff --git a/doc/WebSocketServer.html b/doc/WebSocketServer.html index c97ed102c..3c8ae301b 100644 --- a/doc/WebSocketServer.html +++ b/doc/WebSocketServer.html @@ -2,12 +2,12 @@ - + WebSocketServer - + @@ -39,9 +39,10 @@ Package   Class  + Use  Tree  Deprecated  - Index  + Index  Help  @@ -53,7 +54,7 @@ PREV CLASS  + PREV CLASS   NEXT CLASS FRAMES   @@ -91,7 +92,7 @@

extended by WebSocketServer
-
All Implemented Interfaces:
java.lang.Runnable, WebSocketListener
+
All Implemented Interfaces:
java.lang.Runnable, WebSocketListener, WebSocketProtocol

@@ -102,12 +103,41 @@

WebSocketServer is an abstract class that only takes care of the HTTP handshake portion of WebSockets. It's up to a subclass to add functionality/purpose to the server. + + May be configured to listen to listen on a specified + port using a specified subprotocol. + May also be configured to only allows connections from a specified origin. + Can be configure to support a specific draft of the WebSocket protocol + (DRAFT75 or DRAFT76) or both (AUTO).

+

+
Author:
+
Nathan Rajlich
+

+ + + + + + + +
+Nested Class Summary
+ + + + + + + +
Nested classes/interfaces inherited from interface WebSocketProtocol
WebSocketProtocol.ClientServerType, WebSocketProtocol.Draft
@@ -118,11 +148,12 @@

-private  java.util.concurrent.CopyOnWriteArraySet<WebSocket> -connections +static WebSocketProtocol.Draft +DEFAULT_DRAFT
-          Holds the list of active WebSocket connections. +          If a constructor is used that doesn't define the draft, the DEFAULT_DRAFT + will be the draft of the WebSocket protocl the WebSocketServer supports. @@ -142,21 +173,14 @@

          The value of handshake when a Flash client requests a policy file on this server. - - -private  int -port - -
-          The port number that this WebSocket server should listen on. + +  + + + - - +
Fields inherited from interface WebSocketProtocol
-private  java.nio.channels.ServerSocketChannelserver - -
-          The socket channel for this WebSocket server.
MAX_KEY_VALUE, UTF8_CHARSET
  @@ -181,6 +205,43 @@

          Creates a WebSocketServer that will attempt to listen on port port. + +WebSocketServer(int port, + java.lang.String origin) + +
+          Creates a WebSocketServer that will attempt to listen on port + port. + + +WebSocketServer(int port, + java.lang.String origin, + java.lang.String subprotocol) + +
+          Creates a WebSocketServer that will attempt to listen on port + port using a specified subprotocol. + + +WebSocketServer(int port, + java.lang.String origin, + java.lang.String subprotocol, + java.lang.String draft) + +
+          Creates a WebSocketServer that will attempt to listen on port + port using a specified subprotocol. + + +WebSocketServer(int port, + java.lang.String origin, + java.lang.String subprotocol, + WebSocketProtocol.Draft draft) + +
+          Creates a WebSocketServer that will attempt to listen on port + port using a specified subprotocol. +   @@ -201,6 +262,14 @@

+ WebSocketProtocol.Draft +getDraft() + +
+          Gets the draft that this WebSocketClient supports. + + + protected  java.lang.String getFlashSecurityPolicy() @@ -210,6 +279,15 @@

+ java.lang.String +getOrigin() + +
+          Gets the origin that this WebSocketServer should allow connections + from. + + +  int getPort() @@ -218,6 +296,14 @@

+ java.lang.String +getSubProtocol() + +
+          Gets the subprotocol that this WebSocketClient supports. + + + abstract  void onClientClose(WebSocket conn) @@ -253,8 +339,8 @@

 boolean -onHandshakeRecieved(WebSocket conn, - java.lang.String handshake) +onHandshakeRecieved(WebSocket conn, + WebSocketHandshake handshake)
          Called by a WebSocket instance when a client connection has @@ -316,6 +402,23 @@

 void +setDraft(WebSocketProtocol.Draft draft) + +
+          Sets this WebSocketClient draft. + + + + void +setOrigin(java.lang.String origin) + +
+          Sets the origin that this WebSocketServer should allow connections + from. + + + + void setPort(int port)
@@ -324,6 +427,14 @@

 void +setSubProtocol(java.lang.String subprotocol) + +
+          Sets this WebSocketClient subprotocol. + + + + void start()
@@ -376,6 +487,19 @@


+

+DEFAULT_DRAFT

+
+public static final WebSocketProtocol.Draft DEFAULT_DRAFT
+
+
If a constructor is used that doesn't define the draft, the DEFAULT_DRAFT + will be the draft of the WebSocket protocl the WebSocketServer supports. +

+

+
+
+
+

FLASH_POLICY_REQUEST

@@ -387,76 +511,105 @@ 

See Also:
Constant Field Values
+ + + + + + + + +
+Constructor Detail
+ +

+WebSocketServer

+
+public WebSocketServer()
+
+
Nullary constructor. Creates a WebSocketServer that will attempt to + listen on port DEFAULT_PORT and support both draft 75 and 76 of the + WebSocket protocol. +

+


-

-connections

+

+WebSocketServer

-private final java.util.concurrent.CopyOnWriteArraySet<WebSocket> connections
+public WebSocketServer(int port)
-
Holds the list of active WebSocket connections. "Active" means WebSocket - handshake is complete and socket can be written to, or read from. +
Creates a WebSocketServer that will attempt to listen on port + port.

-
+
Parameters:
port - The port number this server should listen on.

-

-port

+

+WebSocketServer

-private int port
+public WebSocketServer(int port, + java.lang.String origin)
-
The port number that this WebSocket server should listen on. Default is - 80 (HTTP). +
Creates a WebSocketServer that will attempt to listen on port + port. Only allows connections from origin.

-
+
Parameters:
port - The port number this server should listen on.
origin - The origin this server supports.

-

-server

+

+WebSocketServer

-private java.nio.channels.ServerSocketChannel server
+public WebSocketServer(int port, + java.lang.String origin, + java.lang.String subprotocol)
-
The socket channel for this WebSocket server. +
Creates a WebSocketServer that will attempt to listen on port + port using a specified subprotocol. + Only allows connections from origin.

+
Parameters:
port - The port number this server should listen on.
origin - The origin this server supports.
subprotocol - The subprotocol this server supports.
- - - - - - - - - -
-Constructor Detail
+
-

+

WebSocketServer

-public WebSocketServer()
+public WebSocketServer(int port, + java.lang.String origin, + java.lang.String subprotocol, + java.lang.String draft)
-
Nullary constructor. Creates a WebSocketServer that will attempt to - listen on port DEFAULT_PORT. +
Creates a WebSocketServer that will attempt to listen on port + port using a specified subprotocol. + Only allows connections from origin using specified + draft of the WebSocket protocol.

+

+
Parameters:
port - The port number this server should listen on.
origin - The origin this server supports.
subprotocol - The subprotocol this server supports.
draft - The draft of the WebSocket protocol this server supports.

-

+

WebSocketServer

-public WebSocketServer(int port)
+public WebSocketServer(int port, + java.lang.String origin, + java.lang.String subprotocol, + WebSocketProtocol.Draft draft)
Creates a WebSocketServer that will attempt to listen on port - port. + port using a specified subprotocol. + Only allows connections from origin using specified + draft of the WebSocket protocol.

-
Parameters:
port - The port number this server should listen on.
+
Parameters:
port - The port number this server should listen on.
origin - The origin this server supports.
subprotocol - The subprotocol this server supports.
draft - The draft of the WebSocket protocol this server supports.
@@ -469,6 +622,140 @@

+

+setDraft

+
+public void setDraft(WebSocketProtocol.Draft draft)
+
+
Sets this WebSocketClient draft. +

+

+
+
+
+
Parameters:
draft -
+
+
+
+ +

+getDraft

+
+public WebSocketProtocol.Draft getDraft()
+
+
Gets the draft that this WebSocketClient supports. +

+

+
+
+
+ +
Returns:
The draft for this WebSocketClient.
+
+
+
+ +

+setOrigin

+
+public void setOrigin(java.lang.String origin)
+
+
Sets the origin that this WebSocketServer should allow connections + from. +

+

+
+
+
+
Parameters:
origin - The origin to allow connections from.
+
+
+
+ +

+getOrigin

+
+public java.lang.String getOrigin()
+
+
Gets the origin that this WebSocketServer should allow connections + from. +

+

+
+
+
+ +
Returns:
The origin.
+
+
+
+ +

+setPort

+
+public void setPort(int port)
+
+
Sets the port that this WebSocketServer should listen on. +

+

+
+
+
+
Parameters:
port - The port number to listen on.
+
+
+
+ +

+getPort

+
+public int getPort()
+
+
Gets the port number that this server listens on. +

+

+
+
+
+ +
Returns:
The port number.
+
+
+
+ +

+setSubProtocol

+
+public void setSubProtocol(java.lang.String subprotocol)
+
+
Sets this WebSocketClient subprotocol. +

+

+
+
+
+
Parameters:
subprotocol -
+
+
+
+ +

+getSubProtocol

+
+public java.lang.String getSubProtocol()
+
+
Gets the subprotocol that this WebSocketClient supports. +

+

+
+
+
+ +
Returns:
The subprotocol for this WebSocketClient.
+
+
+
+

start

@@ -585,39 +872,6 @@ 


-

-setPort

-
-public void setPort(int port)
-
-
Sets the port that this WebSocketServer should listen on. -

-

-
-
-
-
Parameters:
port - The port number to listen on.
-
-
-
- -

-getPort

-
-public int getPort()
-
-
Gets the port number that this server listens on. -

-

-
-
-
- -
Returns:
The port number.
-
-
-
-

run

@@ -658,11 +912,11 @@ 


-

+

onHandshakeRecieved

 public boolean onHandshakeRecieved(WebSocket conn,
-                                   java.lang.String handshake)
+                                   WebSocketHandshake handshake)
                             throws java.io.IOException
Called by a WebSocket instance when a client connection has @@ -671,7 +925,7 @@

if it is valid, or closes the connection if it is not.

-
Specified by:
onHandshakeRecieved in interface WebSocketListener
+
Specified by:
onHandshakeRecieved in interface WebSocketListener

Parameters:
conn - The WebSocket instance who's handshake has been recieved.
handshake - The entire UTF-8 decoded handshake from the connection. @@ -795,9 +1049,10 @@

Package   Class  + Use  Tree  Deprecated  - Index  + Index  Help  @@ -809,7 +1064,7 @@

PREV CLASS  + PREV CLASS   NEXT CLASS FRAMES   diff --git a/doc/allclasses-frame.html b/doc/allclasses-frame.html index ab615fc3b..e93699c8e 100644 --- a/doc/allclasses-frame.html +++ b/doc/allclasses-frame.html @@ -2,12 +2,12 @@ - + All Classes - + @@ -25,8 +25,16 @@
WebSocketClient
+WebSocketHandshake +
WebSocketListener
+WebSocketProtocol +
+WebSocketProtocol.ClientServerType +
+WebSocketProtocol.Draft +
WebSocketServer
diff --git a/doc/allclasses-noframe.html b/doc/allclasses-noframe.html index 445052d21..672b838c8 100644 --- a/doc/allclasses-noframe.html +++ b/doc/allclasses-noframe.html @@ -2,12 +2,12 @@ - + All Classes - + @@ -25,8 +25,16 @@
WebSocketClient
+WebSocketHandshake +
WebSocketListener
+WebSocketProtocol +
+WebSocketProtocol.ClientServerType +
+WebSocketProtocol.Draft +
WebSocketServer
diff --git a/doc/class-use/WebSocket.html b/doc/class-use/WebSocket.html new file mode 100644 index 000000000..62cdf9173 --- /dev/null +++ b/doc/class-use/WebSocket.html @@ -0,0 +1,332 @@ + + + + + + +Uses of Class WebSocket + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ +
+ + + +
+
+

+Uses of Class
WebSocket

+
+ + + + + +
+Uses of WebSocket in <Unnamed>
+  +

+ + + + + + + + + +
Methods in <Unnamed> that return WebSocket
+ WebSocket[]WebSocketServer.connections() + +
+          Returns a WebSocket[] of currently connected clients.
+  +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Methods in <Unnamed> with parameters of type WebSocket
+abstract  voidWebSocketServer.onClientClose(WebSocket conn) + +
+           
+abstract  voidWebSocketServer.onClientMessage(WebSocket conn, + java.lang.String message) + +
+           
+abstract  voidWebSocketServer.onClientOpen(WebSocket conn) + +
+           
+ voidWebSocketServer.onClose(WebSocket conn) + +
+           
+ voidWebSocketClient.onClose(WebSocket conn) + +
+          Calls subclass' implementation of onClose.
+ voidWebSocketListener.onClose(WebSocket conn) + +
+          Called after WebSocket#close is explicity called, or when the + other end of the WebSocket connection is closed.
+ booleanWebSocketServer.onHandshakeRecieved(WebSocket conn, + WebSocketHandshake handshake) + +
+          Called by a WebSocket instance when a client connection has + finished sending a handshake.
+ booleanWebSocketClient.onHandshakeRecieved(WebSocket conn, + WebSocketHandshake handshake) + +
+          Parses the server's handshake to verify that it's a valid WebSocket + handshake.
+ booleanWebSocketListener.onHandshakeRecieved(WebSocket conn, + WebSocketHandshake handshake) + +
+          Called when the socket connection is first established, and the WebSocket + handshake has been recieved.
+ voidWebSocketServer.onMessage(WebSocket conn, + java.lang.String message) + +
+           
+ voidWebSocketClient.onMessage(WebSocket conn, + java.lang.String message) + +
+          Calls subclass' implementation of onMessage.
+ voidWebSocketListener.onMessage(WebSocket conn, + java.lang.String message) + +
+          Called when an entire text frame has been recieved.
+ voidWebSocketServer.onOpen(WebSocket conn) + +
+           
+ voidWebSocketClient.onOpen(WebSocket conn) + +
+          Calls subclass' implementation of onOpen.
+ voidWebSocketListener.onOpen(WebSocket conn) + +
+          Called after onHandshakeRecieved returns true.
+ voidWebSocketServer.sendToAllExcept(WebSocket connection, + java.lang.String text) + +
+          Sends text to all currently connected WebSocket clients, + except for the specified connection.
+  +

+ + + + + + + + + +
Method parameters in <Unnamed> with type arguments of type WebSocket
+ voidWebSocketServer.sendToAllExcept(java.util.Set<WebSocket> connections, + java.lang.String text) + +
+          Sends text to all currently connected WebSocket clients, + except for those found in the Set connections.
+  +

+


+ + + + + + + + + + + + + + + +
+ +
+ + + +
+ + + diff --git a/doc/class-use/WebSocketClient.html b/doc/class-use/WebSocketClient.html new file mode 100644 index 000000000..747a79831 --- /dev/null +++ b/doc/class-use/WebSocketClient.html @@ -0,0 +1,142 @@ + + + + + + +Uses of Class WebSocketClient + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ +
+ + + +
+
+

+Uses of Class
WebSocketClient

+
+No usage of WebSocketClient +

+


+ + + + + + + + + + + + + + + +
+ +
+ + + +
+ + + diff --git a/doc/class-use/WebSocketHandshake.html b/doc/class-use/WebSocketHandshake.html new file mode 100644 index 000000000..61b02fbed --- /dev/null +++ b/doc/class-use/WebSocketHandshake.html @@ -0,0 +1,187 @@ + + + + + + +Uses of Class WebSocketHandshake + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ +
+ + + +
+
+

+Uses of Class
WebSocketHandshake

+
+ + + + + +
+Uses of WebSocketHandshake in <Unnamed>
+  +

+ + + + + + + + + + + + + + + + + +
Methods in <Unnamed> with parameters of type WebSocketHandshake
+ booleanWebSocketServer.onHandshakeRecieved(WebSocket conn, + WebSocketHandshake handshake) + +
+          Called by a WebSocket instance when a client connection has + finished sending a handshake.
+ booleanWebSocketClient.onHandshakeRecieved(WebSocket conn, + WebSocketHandshake handshake) + +
+          Parses the server's handshake to verify that it's a valid WebSocket + handshake.
+ booleanWebSocketListener.onHandshakeRecieved(WebSocket conn, + WebSocketHandshake handshake) + +
+          Called when the socket connection is first established, and the WebSocket + handshake has been recieved.
+  +

+


+ + + + + + + + + + + + + + + +
+ +
+ + + +
+ + + diff --git a/doc/class-use/WebSocketListener.html b/doc/class-use/WebSocketListener.html new file mode 100644 index 000000000..551475faa --- /dev/null +++ b/doc/class-use/WebSocketListener.html @@ -0,0 +1,191 @@ + + + + + + +Uses of Interface WebSocketListener + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ +
+ + + +
+
+

+Uses of Interface
WebSocketListener

+
+ + + + + +
+Uses of WebSocketListener in <Unnamed>
+  +

+ + + + + + + + + + + + + +
Classes in <Unnamed> that implement WebSocketListener
+ classWebSocketClient + +
+          The WebSocketClient is an abstract class that expects a valid + "ws://" URI to connect to.
+ classWebSocketServer + +
+          WebSocketServer is an abstract class that only takes care of the + HTTP handshake portion of WebSockets.
+  +

+ + + + + + + + +
Constructors in <Unnamed> with parameters of type WebSocketListener
WebSocket(java.nio.channels.SocketChannel socketChannel, + WebSocketListener listener, + WebSocketProtocol.ClientServerType wstype) + +
+          Used in WebSocketServer and WebSocketClient.
+  +

+


+ + + + + + + + + + + + + + + +
+ +
+ + + +
+ + + diff --git a/doc/class-use/WebSocketProtocol.ClientServerType.html b/doc/class-use/WebSocketProtocol.ClientServerType.html new file mode 100644 index 000000000..17e038d83 --- /dev/null +++ b/doc/class-use/WebSocketProtocol.ClientServerType.html @@ -0,0 +1,222 @@ + + + + + + +Uses of Class WebSocketProtocol.ClientServerType + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ +
+ + + +
+
+

+Uses of Class
WebSocketProtocol.ClientServerType

+
+ + + + + +
+Uses of WebSocketProtocol.ClientServerType in <Unnamed>
+  +

+ + + + + + + + + + + + + + + + + +
Methods in <Unnamed> that return WebSocketProtocol.ClientServerType
+ WebSocketProtocol.ClientServerTypeWebSocketHandshake.getType() + +
+           
+static WebSocketProtocol.ClientServerTypeWebSocketProtocol.ClientServerType.valueOf(java.lang.String name) + +
+          Returns the enum constant of this type with the specified name.
+static WebSocketProtocol.ClientServerType[]WebSocketProtocol.ClientServerType.values() + +
+          Returns an array containing the constants of this enum type, in +the order they are declared.
+  +

+ + + + + + + + + +
Methods in <Unnamed> with parameters of type WebSocketProtocol.ClientServerType
+ voidWebSocketHandshake.setType(WebSocketProtocol.ClientServerType handshakeType) + +
+           
+  +

+ + + + + + + + + + + +
Constructors in <Unnamed> with parameters of type WebSocketProtocol.ClientServerType
WebSocket(java.nio.channels.SocketChannel socketChannel, + WebSocketListener listener, + WebSocketProtocol.ClientServerType wstype) + +
+          Used in WebSocketServer and WebSocketClient.
WebSocketHandshake(byte[] handshake, + WebSocketProtocol.ClientServerType type, + WebSocketProtocol.Draft draft) + +
+           
+  +

+


+ + + + + + + + + + + + + + + +
+ +
+ + + +
+ + + diff --git a/doc/class-use/WebSocketProtocol.Draft.html b/doc/class-use/WebSocketProtocol.Draft.html new file mode 100644 index 000000000..e5ec00337 --- /dev/null +++ b/doc/class-use/WebSocketProtocol.Draft.html @@ -0,0 +1,273 @@ + + + + + + +Uses of Class WebSocketProtocol.Draft + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ +
+ + + +
+
+

+Uses of Class
WebSocketProtocol.Draft

+
+ + + + + +
+Uses of WebSocketProtocol.Draft in <Unnamed>
+  +

+ + + + + + + + + +
Fields in <Unnamed> declared as WebSocketProtocol.Draft
+static WebSocketProtocol.DraftWebSocketServer.DEFAULT_DRAFT + +
+          If a constructor is used that doesn't define the draft, the DEFAULT_DRAFT + will be the draft of the WebSocket protocl the WebSocketServer supports.
+  +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Methods in <Unnamed> that return WebSocketProtocol.Draft
+ WebSocketProtocol.DraftWebSocketServer.getDraft() + +
+          Gets the draft that this WebSocketClient supports.
+ WebSocketProtocol.DraftWebSocketClient.getDraft() + +
+          Gets the draft that this WebSocketClient supports.
+ WebSocketProtocol.DraftWebSocketHandshake.getDraft() + +
+           
+static WebSocketProtocol.DraftWebSocketProtocol.Draft.valueOf(java.lang.String name) + +
+          Returns the enum constant of this type with the specified name.
+static WebSocketProtocol.Draft[]WebSocketProtocol.Draft.values() + +
+          Returns an array containing the constants of this enum type, in +the order they are declared.
+  +

+ + + + + + + + + + + + + + + + + +
Methods in <Unnamed> with parameters of type WebSocketProtocol.Draft
+ voidWebSocketServer.setDraft(WebSocketProtocol.Draft draft) + +
+          Sets this WebSocketClient draft.
+ voidWebSocketClient.setDraft(WebSocketProtocol.Draft draft) + +
+          Sets this WebSocketClient draft.
+ voidWebSocketHandshake.setDraft(WebSocketProtocol.Draft handshakeDraft) + +
+           
+  +

+ + + + + + + + + + + +
Constructors in <Unnamed> with parameters of type WebSocketProtocol.Draft
WebSocketHandshake(byte[] handshake, + WebSocketProtocol.ClientServerType type, + WebSocketProtocol.Draft draft) + +
+           
WebSocketServer(int port, + java.lang.String origin, + java.lang.String subprotocol, + WebSocketProtocol.Draft draft) + +
+          Creates a WebSocketServer that will attempt to listen on port + port using a specified subprotocol.
+  +

+


+ + + + + + + + + + + + + + + +
+ +
+ + + +
+ + + diff --git a/doc/class-use/WebSocketProtocol.html b/doc/class-use/WebSocketProtocol.html new file mode 100644 index 000000000..6cfd50d34 --- /dev/null +++ b/doc/class-use/WebSocketProtocol.html @@ -0,0 +1,207 @@ + + + + + + +Uses of Interface WebSocketProtocol + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ +
+ + + +
+
+

+Uses of Interface
WebSocketProtocol

+
+ + + + + +
+Uses of WebSocketProtocol in <Unnamed>
+  +

+ + + + + + + + + +
Subinterfaces of WebSocketProtocol in <Unnamed>
+(package private)  interfaceWebSocketListener + +
+          Implemented by WebSocketClient and WebSocketServer.
+  +

+ + + + + + + + + + + + + + + + + + + + + +
Classes in <Unnamed> that implement WebSocketProtocol
+ classWebSocket + +
+          Represents one end (client or server) of a single WebSocket connection.
+ classWebSocketClient + +
+          The WebSocketClient is an abstract class that expects a valid + "ws://" URI to connect to.
+ classWebSocketHandshake + +
+          The WebSocketHandshake is a class used to create and read client and server handshakes.
+ classWebSocketServer + +
+          WebSocketServer is an abstract class that only takes care of the + HTTP handshake portion of WebSockets.
+  +

+


+ + + + + + + + + + + + + + + +
+ +
+ + + +
+ + + diff --git a/doc/class-use/WebSocketServer.html b/doc/class-use/WebSocketServer.html new file mode 100644 index 000000000..708f86111 --- /dev/null +++ b/doc/class-use/WebSocketServer.html @@ -0,0 +1,142 @@ + + + + + + +Uses of Class WebSocketServer + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ +
+ + + +
+
+

+Uses of Class
WebSocketServer

+
+No usage of WebSocketServer +

+


+ + + + + + + + + + + + + + + +
+ +
+ + + +
+ + + diff --git a/doc/constant-values.html b/doc/constant-values.html index 395799519..b36780f0b 100644 --- a/doc/constant-values.html +++ b/doc/constant-values.html @@ -2,12 +2,12 @@ - + Constant Field Values - + @@ -39,9 +39,10 @@ Package  Class  + Use  Tree  Deprecated  - Index  + Index  Help  @@ -98,7 +99,7 @@

- + + - +
WebSocketWebSocket
@@ -169,9 +170,10 @@

diff --git a/doc/deprecated-list.html b/doc/deprecated-list.html index 76c3ce122..7200416a0 100644 --- a/doc/deprecated-list.html +++ b/doc/deprecated-list.html @@ -2,12 +2,12 @@ - + Deprecated List - + @@ -39,9 +39,10 @@ Package  Class  + Use  Tree   Deprecated  - Index  + Index  Help  @@ -99,9 +100,10 @@

Package  Class  + Use  Tree   Deprecated  - Index  + Index  Help  diff --git a/doc/help-doc.html b/doc/help-doc.html index fa0e26542..4f1502c6b 100644 --- a/doc/help-doc.html +++ b/doc/help-doc.html @@ -2,12 +2,12 @@ - + API Help - + @@ -39,9 +39,10 @@ Package  Class  + Use  Tree  Deprecated  - Index  + Index   Help  @@ -120,6 +121,10 @@

  • Enum declaration
  • Enum description
  • Enum Constant Summary
  • Enum Constant Detail

    +Use

    +
    +Each documented package, class and interface has its own Use page. This page describes what packages, classes, methods, constructors and fields use any part of the given class or package. Given a class or interface A, its Use page includes subclasses of A, fields declared as A, methods that return A, and methods and constructors with parameters of type A. You can access this page by first going to the package, class or interface, then clicking on the "Use" link in the navigation bar.
    +

    Tree (Class Hierarchy)

    There is a Class Hierarchy page for all packages, plus a hierarchy for each package. Each hierarchy page contains a list of classes and a list of interfaces. The classes are organized by inheritance structure starting with java.lang.Object. The interfaces do not inherit from java.lang.Object.
      @@ -132,7 +137,7 @@

      Index

      -The Index contains an alphabetic list of all classes, interfaces, constructors, methods, and fields.
      +The Index contains an alphabetic list of all classes, interfaces, constructors, methods, and fields.

    Prev/Next

    These links take you to the next or previous class, interface, package, or related page.

    @@ -166,9 +171,10 @@

    Package  Class  + Use  Tree  Deprecated  - Index  + Index   Help  diff --git a/doc/index-all.html b/doc/index-all.html deleted file mode 100644 index bfb8c715c..000000000 --- a/doc/index-all.html +++ /dev/null @@ -1,417 +0,0 @@ - - - - - - -Index - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L O P R S U W
    -

    -B

    -
    -
    buffer - -Variable in class WebSocket -
    The 1-byte buffer reused throughout the WebSocket connection to read data. -
    -
    -

    -C

    -
    -
    close() - -Method in class WebSocket -
    Closes the underlying SocketChannel, and calls the listener's onClose - event handler. -
    close() - -Method in class WebSocketClient -
    Calls close on the underlying SocketChannel, which in turn - closes the socket connection, and ends the client socket thread. -
    completeHandshake() - -Method in class WebSocket -
      -
    conn - -Variable in class WebSocketClient -
    The WebSocket instance this client object wraps. -
    connect() - -Method in class WebSocketClient -
    Starts a background thread that attempts and maintains a WebSocket - connection to the URI specified in the constructor or via setURI. -
    connections - -Variable in class WebSocketServer -
    Holds the list of active WebSocket connections. -
    connections() - -Method in class WebSocketServer -
    Returns a WebSocket[] of currently connected clients. -
    CR - -Static variable in class WebSocket -
    The byte representing CR, or Carriage Return, or \r -
    currentFrame - -Variable in class WebSocket -
    The bytes that make up the current text frame being read. -
    -
    -

    -D

    -
    -
    DEFAULT_PORT - -Static variable in class WebSocketServer -
    If the nullary constructor is used, the DEFAULT_PORT will be the port - the WebSocketServer is binded to. -
    -
    -

    -E

    -
    -
    END_OF_FRAME - -Static variable in class WebSocket -
    The byte representing the end of a WebSocket text frame. -
    -
    -

    -F

    -
    -
    FLASH_POLICY_REQUEST - -Static variable in class WebSocketServer -
    The value of handshake when a Flash client requests a policy - file on this server. -
    -
    -

    -G

    -
    -
    getFlashSecurityPolicy() - -Method in class WebSocketServer -
    Gets the XML string that should be returned if a client requests a Flash - security policy. -
    getPort() - -Method in class WebSocketServer -
    Gets the port number that this server listens on. -
    getURI() - -Method in class WebSocketClient -
    Gets the URI that this WebSocketClient is connected to (or should attempt - to connect to). -
    -
    -

    -H

    -
    -
    handleRead() - -Method in class WebSocket -
    Should be called when a Selector has a key that is writable for this - WebSocket's SocketChannel connection. -
    handshakeComplete - -Variable in class WebSocket -
    Internally used to determine whether to recieve data as part of the - remote handshake, or as part of a text frame. -
    -
    -

    -L

    -
    -
    LF - -Static variable in class WebSocket -
    The byte representing LF, or Line Feed, or \n -
    -
    -

    -O

    -
    -
    onClientClose(WebSocket) - -Method in class WebSocketServer -
      -
    onClientMessage(WebSocket, String) - -Method in class WebSocketServer -
      -
    onClientOpen(WebSocket) - -Method in class WebSocketServer -
      -
    onClose(WebSocket) - -Method in class WebSocketClient -
    Calls subclass' implementation of onClose. -
    onClose() - -Method in class WebSocketClient -
      -
    onClose(WebSocket) - -Method in interface WebSocketListener -
    Called after WebSocket#close is explicity called, or when the - other end of the WebSocket connection is closed. -
    onClose(WebSocket) - -Method in class WebSocketServer -
      -
    onHandshakeRecieved(WebSocket, String) - -Method in class WebSocketClient -
    Parses the server's handshake to verify that it's a valid WebSocket - handshake. -
    onHandshakeRecieved(WebSocket, String) - -Method in interface WebSocketListener -
    Called when the socket connection is first established, and the WebSocket - handshake has been recieved. -
    onHandshakeRecieved(WebSocket, String) - -Method in class WebSocketServer -
    Called by a WebSocket instance when a client connection has - finished sending a handshake. -
    onMessage(WebSocket, String) - -Method in class WebSocketClient -
    Calls subclass' implementation of onMessage. -
    onMessage(String) - -Method in class WebSocketClient -
      -
    onMessage(WebSocket, String) - -Method in interface WebSocketListener -
    Called when an entire text frame has been recieved. -
    onMessage(WebSocket, String) - -Method in class WebSocketServer -
      -
    onOpen(WebSocket) - -Method in class WebSocketClient -
    Calls subclass' implementation of onOpen. -
    onOpen() - -Method in class WebSocketClient -
      -
    onOpen(WebSocket) - -Method in interface WebSocketListener -
    Called after onHandshakeRecieved returns true. -
    onOpen(WebSocket) - -Method in class WebSocketServer -
      -
    -
    -

    -P

    -
    -
    port - -Variable in class WebSocketServer -
    The port number that this WebSocket server should listen on. -
    -
    -

    -R

    -
    -
    recieveFrame() - -Method in class WebSocket -
      -
    recieveHandshake() - -Method in class WebSocket -
      -
    remoteHandshake - -Variable in class WebSocket -
    The bytes that make up the remote handshake. -
    run() - -Method in class WebSocketClient -
      -
    run() - -Method in class WebSocketServer -
      -
    -
    -

    -S

    -
    -
    send(String) - -Method in class WebSocket -
      -
    send(String) - -Method in class WebSocketClient -
    Sends text to the connected WebSocket server. -
    sendToAll(String) - -Method in class WebSocketServer -
    Sends text to all currently connected WebSocket clients. -
    sendToAllExcept(WebSocket, String) - -Method in class WebSocketServer -
    Sends text to all currently connected WebSocket clients, - except for the specified connection. -
    sendToAllExcept(Set<WebSocket>, String) - -Method in class WebSocketServer -
    Sends text to all currently connected WebSocket clients, - except for those found in the Set connections. -
    server - -Variable in class WebSocketServer -
    The socket channel for this WebSocket server. -
    setPort(int) - -Method in class WebSocketServer -
    Sets the port that this WebSocketServer should listen on. -
    setURI(URI) - -Method in class WebSocketClient -
    Sets this WebSocketClient to connect to the specified URI. -
    socketChannel - -Variable in class WebSocket -
    The SocketChannel instance to use for this server connection. -
    socketChannel() - -Method in class WebSocket -
      -
    start() - -Method in class WebSocketServer -
    Starts the server thread that binds to the currently set port number and - listeners for WebSocket connection requests. -
    START_OF_FRAME - -Static variable in class WebSocket -
    The byte representing the beginning of a WebSocket text frame. -
    stop() - -Method in class WebSocketServer -
    Closes all connected clients sockets, then closes the underlying - ServerSocketChannel, effectively killing the server socket thread and - freeing the port the server was bound to. -
    -
    -

    -U

    -
    -
    uri - -Variable in class WebSocketClient -
    The URI this client is supposed to connect to. -
    UTF8_CHARSET - -Static variable in class WebSocket -
    The WebSocket protocol expects UTF-8 encoded bytes. -
    -
    -

    -W

    -
    -
    WebSocket - Class in <Unnamed>
    Represents one end (client or server) of a single WebSocket connection.
    WebSocket(SocketChannel, WebSocketListener) - -Constructor for class WebSocket -
    Used in WebSocketServer and WebSocketClient. -
    WebSocketClient - Class in <Unnamed>
    The WebSocketClient is an abstract class that expects a valid - "ws://" URI to connect to.
    WebSocketClient() - -Constructor for class WebSocketClient -
    Nullary constructor. -
    WebSocketClient(URI) - -Constructor for class WebSocketClient -
    Constructs a WebSocketClient instance and sets it to the connect to the - specified URI. -
    WebSocketListener - Interface in <Unnamed>
    Implemented by WebSocketClient and WebSocketServer.
    WebSocketServer - Class in <Unnamed>
    WebSocketServer is an abstract class that only takes care of the - HTTP handshake portion of WebSockets.
    WebSocketServer() - -Constructor for class WebSocketServer -
    Nullary constructor. -
    WebSocketServer(int) - -Constructor for class WebSocketServer -
    Creates a WebSocketServer that will attempt to listen on port - port. -
    wsl - -Variable in class WebSocket -
    The listener to notify of WebSocket events. -
    -
    -B C D E F G H L O P R S U W - - - - - - - - - - - - - - -
    - -
    - - - -
    - - - diff --git a/doc/index-files/index-1.html b/doc/index-files/index-1.html new file mode 100644 index 000000000..a3a1de44a --- /dev/null +++ b/doc/index-files/index-1.html @@ -0,0 +1,144 @@ + + + + + + +B-Index + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    +

    +B

    +
    +
    buildHandshake() - +Method in class WebSocketHandshake +
    Generates the handshake byte array based on the handshake draft and type as well as the keys + set for this handshake. +
    +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    + + + diff --git a/doc/index-files/index-10.html b/doc/index-files/index-10.html new file mode 100644 index 000000000..c1816543f --- /dev/null +++ b/doc/index-files/index-10.html @@ -0,0 +1,198 @@ + + + + + + +O-Index + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    +

    +O

    +
    +
    onClientClose(WebSocket) - +Method in class WebSocketServer +
      +
    onClientMessage(WebSocket, String) - +Method in class WebSocketServer +
      +
    onClientOpen(WebSocket) - +Method in class WebSocketServer +
      +
    onClose(WebSocket) - +Method in class WebSocketClient +
    Calls subclass' implementation of onClose. +
    onClose() - +Method in class WebSocketClient +
      +
    onClose(WebSocket) - +Method in interface WebSocketListener +
    Called after WebSocket#close is explicity called, or when the + other end of the WebSocket connection is closed. +
    onClose(WebSocket) - +Method in class WebSocketServer +
      +
    onHandshakeRecieved(WebSocket, WebSocketHandshake) - +Method in class WebSocketClient +
    Parses the server's handshake to verify that it's a valid WebSocket + handshake. +
    onHandshakeRecieved(WebSocket, WebSocketHandshake) - +Method in interface WebSocketListener +
    Called when the socket connection is first established, and the WebSocket + handshake has been recieved. +
    onHandshakeRecieved(WebSocket, WebSocketHandshake) - +Method in class WebSocketServer +
    Called by a WebSocket instance when a client connection has + finished sending a handshake. +
    onMessage(WebSocket, String) - +Method in class WebSocketClient +
    Calls subclass' implementation of onMessage. +
    onMessage(String) - +Method in class WebSocketClient +
      +
    onMessage(WebSocket, String) - +Method in interface WebSocketListener +
    Called when an entire text frame has been recieved. +
    onMessage(WebSocket, String) - +Method in class WebSocketServer +
      +
    onOpen(WebSocket) - +Method in class WebSocketClient +
    Calls subclass' implementation of onOpen. +
    onOpen() - +Method in class WebSocketClient +
      +
    onOpen(WebSocket) - +Method in interface WebSocketListener +
    Called after onHandshakeRecieved returns true. +
    onOpen(WebSocket) - +Method in class WebSocketServer +
      +
    +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    + + + diff --git a/doc/index-files/index-11.html b/doc/index-files/index-11.html new file mode 100644 index 000000000..d9b71f6ef --- /dev/null +++ b/doc/index-files/index-11.html @@ -0,0 +1,143 @@ + + + + + + +P-Index + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    +

    +P

    +
    +
    parseHandshake() - +Method in class WebSocketHandshake +
    Parses the parts of the handshake into Hashtable keys. +
    +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    + + + diff --git a/doc/index-files/index-12.html b/doc/index-files/index-12.html new file mode 100644 index 000000000..3ed8771a0 --- /dev/null +++ b/doc/index-files/index-12.html @@ -0,0 +1,146 @@ + + + + + + +R-Index + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    +

    +R

    +
    +
    run() - +Method in class WebSocketClient +
      +
    run() - +Method in class WebSocketServer +
      +
    +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    + + + diff --git a/doc/index-files/index-13.html b/doc/index-files/index-13.html new file mode 100644 index 000000000..22320ee6a --- /dev/null +++ b/doc/index-files/index-13.html @@ -0,0 +1,203 @@ + + + + + + +S-Index + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    +

    +S

    +
    +
    send(String) - +Method in class WebSocket +
      +
    send(String) - +Method in class WebSocketClient +
    Sends text to the connected WebSocket server. +
    sendToAll(String) - +Method in class WebSocketServer +
    Sends text to all currently connected WebSocket clients. +
    sendToAllExcept(WebSocket, String) - +Method in class WebSocketServer +
    Sends text to all currently connected WebSocket clients, + except for the specified connection. +
    sendToAllExcept(Set<WebSocket>, String) - +Method in class WebSocketServer +
    Sends text to all currently connected WebSocket clients, + except for those found in the Set connections. +
    setDraft(WebSocketProtocol.Draft) - +Method in class WebSocketClient +
    Sets this WebSocketClient draft. +
    setDraft(WebSocketProtocol.Draft) - +Method in class WebSocketHandshake +
      +
    setDraft(WebSocketProtocol.Draft) - +Method in class WebSocketServer +
    Sets this WebSocketClient draft. +
    setHandshake(byte[]) - +Method in class WebSocketHandshake +
      +
    setOrigin(String) - +Method in class WebSocketServer +
    Sets the origin that this WebSocketServer should allow connections + from. +
    setPort(int) - +Method in class WebSocketServer +
    Sets the port that this WebSocketServer should listen on. +
    setSubProtocol(String) - +Method in class WebSocketClient +
    Sets this WebSocketClient subprotocol. +
    setSubProtocol(String) - +Method in class WebSocketServer +
    Sets this WebSocketClient subprotocol. +
    setType(WebSocketProtocol.ClientServerType) - +Method in class WebSocketHandshake +
      +
    setURI(URI) - +Method in class WebSocketClient +
    Sets this WebSocketClient to connect to the specified URI. +
    socketChannel() - +Method in class WebSocket +
      +
    start() - +Method in class WebSocketServer +
    Starts the server thread that binds to the currently set port number and + listeners for WebSocket connection requests. +
    START_OF_FRAME - +Static variable in class WebSocket +
    The byte representing the beginning of a WebSocket text frame. +
    stop() - +Method in class WebSocketServer +
    Closes all connected clients sockets, then closes the underlying + ServerSocketChannel, effectively killing the server socket thread and + freeing the port the server was bound to. +
    +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    + + + diff --git a/doc/index-files/index-14.html b/doc/index-files/index-14.html new file mode 100644 index 000000000..13856c032 --- /dev/null +++ b/doc/index-files/index-14.html @@ -0,0 +1,143 @@ + + + + + + +T-Index + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    +

    +T

    +
    +
    toString() - +Method in class WebSocketHandshake +
      +
    +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    + + + diff --git a/doc/index-files/index-15.html b/doc/index-files/index-15.html new file mode 100644 index 000000000..d893e9499 --- /dev/null +++ b/doc/index-files/index-15.html @@ -0,0 +1,143 @@ + + + + + + +U-Index + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    +

    +U

    +
    +
    UTF8_CHARSET - +Static variable in interface WebSocketProtocol +
    The WebSocket protocol expects UTF-8 encoded bytes. +
    +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    + + + diff --git a/doc/index-files/index-16.html b/doc/index-files/index-16.html new file mode 100644 index 000000000..6fbd829da --- /dev/null +++ b/doc/index-files/index-16.html @@ -0,0 +1,154 @@ + + + + + + +V-Index + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    +

    +V

    +
    +
    valueOf(String) - +Static method in enum WebSocketProtocol.ClientServerType +
    Returns the enum constant of this type with the specified name. +
    valueOf(String) - +Static method in enum WebSocketProtocol.Draft +
    Returns the enum constant of this type with the specified name. +
    values() - +Static method in enum WebSocketProtocol.ClientServerType +
    Returns an array containing the constants of this enum type, in +the order they are declared. +
    values() - +Static method in enum WebSocketProtocol.Draft +
    Returns an array containing the constants of this enum type, in +the order they are declared. +
    +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    + + + diff --git a/doc/index-files/index-17.html b/doc/index-files/index-17.html new file mode 100644 index 000000000..4b05322f8 --- /dev/null +++ b/doc/index-files/index-17.html @@ -0,0 +1,192 @@ + + + + + + +W-Index + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    +

    +W

    +
    +
    WebSocket - Class in <Unnamed>
    Represents one end (client or server) of a single WebSocket connection.
    WebSocket(SocketChannel, WebSocketListener, WebSocketProtocol.ClientServerType) - +Constructor for class WebSocket +
    Used in WebSocketServer and WebSocketClient. +
    WebSocketClient - Class in <Unnamed>
    The WebSocketClient is an abstract class that expects a valid + "ws://" URI to connect to.
    WebSocketClient() - +Constructor for class WebSocketClient +
    Nullary constructor. +
    WebSocketClient(URI) - +Constructor for class WebSocketClient +
    Constructs a WebSocketClient instance and sets it to the connect to the + specified URI. +
    WebSocketClient(URI, String) - +Constructor for class WebSocketClient +
    Constructs a WebSocketClient instance and sets it to the connect to the + specified URI using the specified subprotocol. +
    WebSocketClient(URI, String, String) - +Constructor for class WebSocketClient +
    Constructs a WebSocketClient instance and sets it to the connect to the + specified URI using the specified subprotocol and draft. +
    WebSocketHandshake - Class in <Unnamed>
    The WebSocketHandshake is a class used to create and read client and server handshakes.
    WebSocketHandshake() - +Constructor for class WebSocketHandshake +
      +
    WebSocketHandshake(byte[]) - +Constructor for class WebSocketHandshake +
      +
    WebSocketHandshake(byte[], WebSocketProtocol.ClientServerType, WebSocketProtocol.Draft) - +Constructor for class WebSocketHandshake +
      +
    WebSocketListener - Interface in <Unnamed>
    Implemented by WebSocketClient and WebSocketServer.
    WebSocketProtocol - Interface in <Unnamed>
    Constants used by WebSocket protocol.
    WebSocketProtocol.ClientServerType - Enum in <Unnamed>
    The type of WebSocket
    WebSocketProtocol.Draft - Enum in <Unnamed>
    The version of the WebSocket Internet-Draft
    WebSocketServer - Class in <Unnamed>
    WebSocketServer is an abstract class that only takes care of the + HTTP handshake portion of WebSockets.
    WebSocketServer() - +Constructor for class WebSocketServer +
    Nullary constructor. +
    WebSocketServer(int) - +Constructor for class WebSocketServer +
    Creates a WebSocketServer that will attempt to listen on port + port. +
    WebSocketServer(int, String) - +Constructor for class WebSocketServer +
    Creates a WebSocketServer that will attempt to listen on port + port. +
    WebSocketServer(int, String, String) - +Constructor for class WebSocketServer +
    Creates a WebSocketServer that will attempt to listen on port + port using a specified subprotocol. +
    WebSocketServer(int, String, String, String) - +Constructor for class WebSocketServer +
    Creates a WebSocketServer that will attempt to listen on port + port using a specified subprotocol. +
    WebSocketServer(int, String, String, WebSocketProtocol.Draft) - +Constructor for class WebSocketServer +
    Creates a WebSocketServer that will attempt to listen on port + port using a specified subprotocol. +
    +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    + + + diff --git a/doc/index-files/index-2.html b/doc/index-files/index-2.html new file mode 100644 index 000000000..d128c3eea --- /dev/null +++ b/doc/index-files/index-2.html @@ -0,0 +1,158 @@ + + + + + + +C-Index + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    +

    +C

    +
    +
    close() - +Method in class WebSocket +
    Closes the underlying SocketChannel, and calls the listener's onClose + event handler. +
    close() - +Method in class WebSocketClient +
    Calls close on the underlying SocketChannel, which in turn + closes the socket connection, and ends the client socket thread. +
    connect() - +Method in class WebSocketClient +
    Starts a background thread that attempts and maintains a WebSocket + connection to the URI specified in the constructor or via setURI. +
    connections() - +Method in class WebSocketServer +
    Returns a WebSocket[] of currently connected clients. +
    CR - +Static variable in class WebSocket +
    The byte representing CR, or Carriage Return, or \r +
    +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    + + + diff --git a/doc/index-files/index-3.html b/doc/index-files/index-3.html new file mode 100644 index 000000000..0e08576f3 --- /dev/null +++ b/doc/index-files/index-3.html @@ -0,0 +1,148 @@ + + + + + + +D-Index + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    +

    +D

    +
    +
    DEFAULT_DRAFT - +Static variable in class WebSocketServer +
    If a constructor is used that doesn't define the draft, the DEFAULT_DRAFT + will be the draft of the WebSocket protocl the WebSocketServer supports. +
    DEFAULT_PORT - +Static variable in class WebSocketServer +
    If the nullary constructor is used, the DEFAULT_PORT will be the port + the WebSocketServer is binded to. +
    +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    + + + diff --git a/doc/index-files/index-4.html b/doc/index-files/index-4.html new file mode 100644 index 000000000..e13437a28 --- /dev/null +++ b/doc/index-files/index-4.html @@ -0,0 +1,143 @@ + + + + + + +E-Index + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    +

    +E

    +
    +
    END_OF_FRAME - +Static variable in class WebSocket +
    The byte representing the end of a WebSocket text frame. +
    +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    + + + diff --git a/doc/index-files/index-5.html b/doc/index-files/index-5.html new file mode 100644 index 000000000..bf8bbae66 --- /dev/null +++ b/doc/index-files/index-5.html @@ -0,0 +1,144 @@ + + + + + + +F-Index + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    +

    +F

    +
    +
    FLASH_POLICY_REQUEST - +Static variable in class WebSocketServer +
    The value of handshake when a Flash client requests a policy + file on this server. +
    +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    + + + diff --git a/doc/index-files/index-6.html b/doc/index-files/index-6.html new file mode 100644 index 000000000..301b13864 --- /dev/null +++ b/doc/index-files/index-6.html @@ -0,0 +1,182 @@ + + + + + + +G-Index + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    +

    +G

    +
    +
    getAsByteArray(String) - +Method in class WebSocketHandshake +
      +
    getDraft() - +Method in class WebSocketClient +
    Gets the draft that this WebSocketClient supports. +
    getDraft() - +Method in class WebSocketHandshake +
      +
    getDraft() - +Method in class WebSocketServer +
    Gets the draft that this WebSocketClient supports. +
    getFlashSecurityPolicy() - +Method in class WebSocketServer +
    Gets the XML string that should be returned if a client requests a Flash + security policy. +
    getHandshake() - +Method in class WebSocketHandshake +
      +
    getOrigin() - +Method in class WebSocketServer +
    Gets the origin that this WebSocketServer should allow connections + from. +
    getPort() - +Method in class WebSocketServer +
    Gets the port number that this server listens on. +
    getProperty(String) - +Method in class WebSocketHandshake +
      +
    getSubProtocol() - +Method in class WebSocketClient +
    Gets the subprotocol that this WebSocketClient supports. +
    getSubProtocol() - +Method in class WebSocketServer +
    Gets the subprotocol that this WebSocketClient supports. +
    getType() - +Method in class WebSocketHandshake +
      +
    getURI() - +Method in class WebSocketClient +
    Gets the URI that this WebSocketClient is connected to (or should attempt + to connect to). +
    +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    + + + diff --git a/doc/index-files/index-7.html b/doc/index-files/index-7.html new file mode 100644 index 000000000..6c6b8c124 --- /dev/null +++ b/doc/index-files/index-7.html @@ -0,0 +1,144 @@ + + + + + + +H-Index + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    +

    +H

    +
    +
    handleRead() - +Method in class WebSocket +
    Should be called when a Selector has a key that is writable for this + WebSocket's SocketChannel connection. +
    +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    + + + diff --git a/doc/index-files/index-8.html b/doc/index-files/index-8.html new file mode 100644 index 000000000..0592ab2fe --- /dev/null +++ b/doc/index-files/index-8.html @@ -0,0 +1,143 @@ + + + + + + +L-Index + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    +

    +L

    +
    +
    LF - +Static variable in class WebSocket +
    The byte representing LF, or Line Feed, or \n +
    +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    + + + diff --git a/doc/index-files/index-9.html b/doc/index-files/index-9.html new file mode 100644 index 000000000..8df3f9d7b --- /dev/null +++ b/doc/index-files/index-9.html @@ -0,0 +1,143 @@ + + + + + + +M-Index + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    +

    +M

    +
    +
    MAX_KEY_VALUE - +Static variable in interface WebSocketProtocol +
    The maximum value of a WebSocket key. +
    +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G H L M O P R S T U V W
    + + + diff --git a/doc/index.html b/doc/index.html index e70552c6e..22c564e35 100644 --- a/doc/index.html +++ b/doc/index.html @@ -2,7 +2,7 @@ - + Generated Documentation (Untitled) diff --git a/doc/overview-tree.html b/doc/overview-tree.html index 96a82046e..785067d11 100644 --- a/doc/overview-tree.html +++ b/doc/overview-tree.html @@ -2,12 +2,12 @@ - + Class Hierarchy - + @@ -39,9 +39,10 @@ Package  Class  + Use   Tree  Deprecated  - Index  + Index  Help  @@ -86,7 +87,14 @@

    @@ -94,7 +102,19 @@

    Interface Hierarchy

    +

    +Enum Hierarchy +

    +
    @@ -109,9 +129,10 @@

    Package  Class  + Use   Tree  Deprecated  - Index  + Index  Help  diff --git a/doc/package-frame.html b/doc/package-frame.html index 6ec1e46dd..0798ef96c 100644 --- a/doc/package-frame.html +++ b/doc/package-frame.html @@ -2,12 +2,12 @@ - + <Unnamed> - + @@ -23,7 +23,9 @@ Interfaces 
    -WebSocketListener
    +WebSocketListener +
    +WebSocketProtocol @@ -38,10 +40,25 @@
    WebSocketClient
    +WebSocketHandshake +
    WebSocketServer + + + + +
    +Enums  + +
    +WebSocketProtocol.ClientServerType +
    +WebSocketProtocol.Draft
    + + diff --git a/doc/package-summary.html b/doc/package-summary.html index bc9b72a47..6feaf9ec6 100644 --- a/doc/package-summary.html +++ b/doc/package-summary.html @@ -2,12 +2,12 @@ - + - + @@ -29,9 +29,10 @@ Package  Class  + Use  Tree  Deprecated  - Index  + Index  Help  @@ -80,6 +81,10 @@

    WebSocketListener Implemented by WebSocketClient and WebSocketServer. + +WebSocketProtocol +Constants used by WebSocket protocol. +   @@ -100,6 +105,10 @@

    "ws://" URI to connect to. +WebSocketHandshake +The WebSocketHandshake is a class used to create and read client and server handshakes. + + WebSocketServer WebSocketServer is an abstract class that only takes care of the HTTP handshake portion of WebSockets. @@ -107,6 +116,24 @@

      +

    + + + + + + + + + + + + + +
    +Enum Summary
    WebSocketProtocol.ClientServerTypeThe type of WebSocket
    WebSocketProtocol.DraftThe version of the WebSocket Internet-Draft
    +  +

    @@ -124,9 +151,10 @@

    Package  Class  + Use  Tree  Deprecated  - Index  + Index  Help  diff --git a/doc/package-tree.html b/doc/package-tree.html index c9a7f24c3..d3f677590 100644 --- a/doc/package-tree.html +++ b/doc/package-tree.html @@ -2,12 +2,12 @@ - + Class Hierarchy - + @@ -39,9 +39,10 @@ Package  Class  + Use   Tree  Deprecated  - Index  + Index  Help  @@ -87,7 +88,14 @@

    @@ -95,7 +103,19 @@

    Interface Hierarchy

    +

    +Enum Hierarchy +

    +
    @@ -110,9 +130,10 @@

    Package  Class  + Use   Tree  Deprecated  - Index  + Index  Help  diff --git a/doc/package-use.html b/doc/package-use.html new file mode 100644 index 000000000..7266de237 --- /dev/null +++ b/doc/package-use.html @@ -0,0 +1,155 @@ + + + + + + +Uses of Package + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +
    +
    +

    +Uses of Package

    +
    + + + + + + + + +
    +Classes in <Unnamed> used by <Unnamed>
    WebSocket + +
    +          Represents one end (client or server) of a single WebSocket connection.
    +  +

    +


    + + + + + + + + + + + + + + + +
    + +
    + + + +
    + + + diff --git a/doc/serialized-form.html b/doc/serialized-form.html new file mode 100644 index 000000000..3c4473be6 --- /dev/null +++ b/doc/serialized-form.html @@ -0,0 +1,205 @@ + + + + + + +Serialized Form + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +
    +
    +

    +Serialized Form

    +
    +
    + + + + + +
    +Package <Unnamed>
    + +

    + + + + + +
    +Class WebSocketHandshake extends java.util.Hashtable<java.lang.String,java.lang.Object> implements Serializable
    + +

    +serialVersionUID: -280930290679993232L + +

    + + + + + +
    +Serialized Fields
    + +

    +handshake

    +
    +byte[] handshake
    +
    +
    The raw handshake. +

    +

    +
    +
    +
    +

    +handshakeDraft

    +
    +WebSocketProtocol.Draft handshakeDraft
    +
    +
    The WebSocket Internet-Draft version. +

    +

    +
    +
    +
    +

    +handshakeType

    +
    +WebSocketProtocol.ClientServerType handshakeType
    +
    +
    The type of the handshake. The refers to the source of the handshake, either + client or server. +

    +

    +
    +
    + +

    +


    + + + + + + + + + + + + + + + +
    + +
    + + + +
    + + + From 6cdecbbb24a56b40ce051731ab032cd9a41f0a40 Mon Sep 17 00:00:00 2001 From: Nathan Mische Date: Mon, 26 Jul 2010 10:23:57 -0400 Subject: [PATCH 06/12] Updating README. --- README.markdown | 1 - 1 file changed, 1 deletion(-) diff --git a/README.markdown b/README.markdown index 0714fe027..9259f5739 100644 --- a/README.markdown +++ b/README.markdown @@ -1,6 +1,5 @@ Java WebSockets =============== -### CAUTION/TODO: This WebSocket implementation adheres to an older draft of the WebSocket specification (draft 75). The [current draft](http://www.whatwg.org/specs/web-socket-protocol/) (draft 76) is a TODO item/fork-worthy! ### This repository contains a simple WebSocket server and client implementation in Java. The underlying classes use the Java classes `ServerSocketChannel` and From 9f4348e3e08099a9cff6a9f7c37a8abb15271357 Mon Sep 17 00:00:00 2001 From: Nathan Mische Date: Mon, 26 Jul 2010 10:38:41 -0400 Subject: [PATCH 07/12] Whitespace cleanup. --- src/WebSocket.java | 52 ++-- src/WebSocketClient.java | 280 +++++++++---------- src/WebSocketHandshake.java | 522 ++++++++++++++++++------------------ src/WebSocketProtocol.java | 44 +-- src/WebSocketServer.java | 153 ++++++----- 5 files changed, 525 insertions(+), 526 deletions(-) diff --git a/src/WebSocket.java b/src/WebSocket.java index bdd29146a..f26649e93 100644 --- a/src/WebSocket.java +++ b/src/WebSocket.java @@ -183,46 +183,46 @@ private void recieveHandshake() throws IOException { // Check to see if this is a flash policy request if (h.length==23 && h[h.length-1] == 0) { - handshake = new WebSocketHandshake(h); - completeHandshake(handshake); - return; + handshake = new WebSocketHandshake(h); + completeHandshake(handshake); + return; } Draft draft; String hsString = new String(this.remoteHandshakeBuffer.array(), UTF8_CHARSET); if (hsString.toLowerCase().contains("\r\nsec-")){ - draft = Draft.DRAFT76; + draft = Draft.DRAFT76; } else { - draft = Draft.DRAFT75; + draft = Draft.DRAFT75; } if (draft == Draft.DRAFT75 - &&h.length>=4 - && h[h.length-4] == CR + &&h.length>=4 + && h[h.length-4] == CR && h[h.length-3] == LF && h[h.length-2] == CR && h[h.length-1] == LF) { - - ClientServerType type = (wstype == ClientServerType.CLIENT) ? ClientServerType.SERVER : ClientServerType.CLIENT; - handshake = new WebSocketHandshake(h, type, draft); - completeHandshake(handshake); - return; - + + ClientServerType type = (wstype == ClientServerType.CLIENT) ? ClientServerType.SERVER : ClientServerType.CLIENT; + handshake = new WebSocketHandshake(h, type, draft); + completeHandshake(handshake); + return; + } if (draft == Draft.DRAFT76 - && wstype == ClientServerType.SERVER - && h.length>=12 - && h[h.length-12] == CR - && h[h.length-11] == LF + && wstype == ClientServerType.SERVER + && h.length>=12 + && h[h.length-12] == CR + && h[h.length-11] == LF && h[h.length-10] == CR && h[h.length-9] == LF) { - - handshake = new WebSocketHandshake(h, ClientServerType.CLIENT, draft); - completeHandshake(handshake); - return; - + + handshake = new WebSocketHandshake(h, ClientServerType.CLIENT, draft); + completeHandshake(handshake); + return; + } if (draft == Draft.DRAFT76 @@ -232,10 +232,10 @@ private void recieveHandshake() throws IOException { && h[h.length-19] == LF && h[h.length-18] == CR && h[h.length-17] == LF) { - - handshake = new WebSocketHandshake(h, ClientServerType.SERVER, draft); - completeHandshake(handshake); - return; + + handshake = new WebSocketHandshake(h, ClientServerType.SERVER, draft); + completeHandshake(handshake); + return; } } diff --git a/src/WebSocketClient.java b/src/WebSocketClient.java index 5a34efb50..f6e0d0011 100644 --- a/src/WebSocketClient.java +++ b/src/WebSocketClient.java @@ -246,60 +246,60 @@ public void run() { public boolean onHandshakeRecieved(WebSocket conn, WebSocketHandshake handshake) throws IOException { if (handshake.getDraft() == Draft.DRAFT75) { - // TODO: Do some parsing of the returned handshake, and close connection + // TODO: Do some parsing of the returned handshake, and close connection // (return false) if we recieved anything unexpected. - return true; + return true; } else if (handshake.getDraft() == Draft.DRAFT76) { - + if (!handshake.getProperty("status-code").equals("101")){ - return false; + return false; + } + + if (!handshake.containsKey("upgrade") || !handshake.getProperty("upgrade").equalsIgnoreCase("websocket")) { + return false; + } + + if (!handshake.containsKey("connection") || !handshake.getProperty("connection").equalsIgnoreCase("upgrade")) { + return false; + } + + if (!handshake.containsKey("sec-websocket-origin") || !handshake.getProperty("sec-websocket-origin").equalsIgnoreCase("null")) { + return false; } - if (!handshake.containsKey("upgrade") || !handshake.getProperty("upgrade").equalsIgnoreCase("websocket")) { - return false; - } - - if (!handshake.containsKey("connection") || !handshake.getProperty("connection").equalsIgnoreCase("upgrade")) { - return false; - } - - if (!handshake.containsKey("sec-websocket-origin") || !handshake.getProperty("sec-websocket-origin").equalsIgnoreCase("null")) { - return false; - } - - int port = (uri.getPort() == -1) ? 80 : uri.getPort(); - String location = "ws://"; - location += uri.getHost() + (port != 80 ? ":" + port : ""); - location += "/" + uri.getPath(); - - if (!handshake.containsKey("sec-websocket-location") || !handshake.getProperty("sec-websocket-location").equalsIgnoreCase(location)) { - return false; - } - - if (subprotocol != null){ - // TODO: support lists of protocols - if (!handshake.containsKey("sec-websocket-protocol") || !handshake.getProperty("sec-websocket-protocol").equals(subprotocol)) { - return false; - } - } - - - if (expected == null) { - try { - MessageDigest md = MessageDigest.getInstance("MD5"); - expected = md.digest(challenge); - } catch (NoSuchAlgorithmException e) { - return false; - } - } + int port = (uri.getPort() == -1) ? 80 : uri.getPort(); + String location = "ws://"; + location += uri.getHost() + (port != 80 ? ":" + port : ""); + location += "/" + uri.getPath(); + + if (!handshake.containsKey("sec-websocket-location") || !handshake.getProperty("sec-websocket-location").equalsIgnoreCase(location)) { + return false; + } + + if (subprotocol != null){ + // TODO: support lists of protocols + if (!handshake.containsKey("sec-websocket-protocol") || !handshake.getProperty("sec-websocket-protocol").equals(subprotocol)) { + return false; + } + } + + + if (expected == null) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + expected = md.digest(challenge); + } catch (NoSuchAlgorithmException e) { + return false; + } + } byte[] reply = handshake.getAsByteArray("response"); - + if (!Arrays.equals(expected, reply)) { - return false; + return false; } - - return true; + + return true; } // If we get here, then something must be wrong @@ -311,12 +311,12 @@ public boolean onHandshakeRecieved(WebSocket conn, WebSocketHandshake handshake) * @throws IOException */ private void sendClientHandshake(WebSocket conn) throws IOException { - - int port = (uri.getPort() == -1) ? 80 : uri.getPort(); - String requestURI = "/" + uri.getPath(); + + int port = (uri.getPort() == -1) ? 80 : uri.getPort(); + String requestURI = "/" + uri.getPath(); String host = uri.getHost() + (port != 80 ? ":" + port : ""); String origin = "null"; - + WebSocketHandshake clientHandshake = new WebSocketHandshake(); clientHandshake.setType(ClientServerType.CLIENT); clientHandshake.setDraft(getDraft()); @@ -324,72 +324,72 @@ private void sendClientHandshake(WebSocket conn) throws IOException { clientHandshake.put("host", host); clientHandshake.put("origin", origin); if (subprotocol != null) { - if (getDraft() == Draft.DRAFT75) { - clientHandshake.put("websocket-protocol", subprotocol); - } else { - clientHandshake.put("sec-webSocket-protocol", subprotocol); - } + if (getDraft() == Draft.DRAFT75) { + clientHandshake.put("websocket-protocol", subprotocol); + } else { + clientHandshake.put("sec-webSocket-protocol", subprotocol); + } } - + if (getDraft() == Draft.DRAFT76) { - - Random rand = new Random(); - - int spaces1 = rand.nextInt(11); - int spaces2 = rand.nextInt(11); - - spaces1+=2; - spaces2+=2; - - int max1 = Integer.MAX_VALUE / spaces1; - int max2 = Integer.MAX_VALUE / spaces2; - - int number1 = rand.nextInt(max1+1); - int number2 = rand.nextInt(max2+1); - - Integer product1 = number1 * spaces1; - Integer product2 = number2 * spaces2; - - String key1 = product1.toString(); - String key2 = product2.toString(); - - key1 = addNoise(key1); - key2 = addNoise(key2); - - key1 = addSpaces(key1,spaces1); - key2 = addSpaces(key2,spaces2); - - clientHandshake.put("sec-websocket-key1", key1); - clientHandshake.put("sec-websocket-key2", key2); - - byte[] key3 = new byte[8]; - rand.nextBytes(key3); - - clientHandshake.put("key3", key3); - - challenge = new byte[] { - (byte)(number1 >> 24), - (byte)(number1 >> 16), - (byte)(number1 >> 8), - (byte)(number1 >> 0), - (byte)(number2 >> 24), - (byte)(number2 >> 16), - (byte)(number2 >> 8), - (byte)(number2 >> 0), - (byte) key3[0], - (byte) key3[1], - (byte) key3[2], - (byte) key3[3], - (byte) key3[4], - (byte) key3[5], - (byte) key3[6], - (byte) key3[7] + + Random rand = new Random(); + + int spaces1 = rand.nextInt(11); + int spaces2 = rand.nextInt(11); + + spaces1+=2; + spaces2+=2; + + int max1 = Integer.MAX_VALUE / spaces1; + int max2 = Integer.MAX_VALUE / spaces2; + + int number1 = rand.nextInt(max1+1); + int number2 = rand.nextInt(max2+1); + + Integer product1 = number1 * spaces1; + Integer product2 = number2 * spaces2; + + String key1 = product1.toString(); + String key2 = product2.toString(); + + key1 = addNoise(key1); + key2 = addNoise(key2); + + key1 = addSpaces(key1,spaces1); + key2 = addSpaces(key2,spaces2); + + clientHandshake.put("sec-websocket-key1", key1); + clientHandshake.put("sec-websocket-key2", key2); + + byte[] key3 = new byte[8]; + rand.nextBytes(key3); + + clientHandshake.put("key3", key3); + + challenge = new byte[] { + (byte)(number1 >> 24), + (byte)(number1 >> 16), + (byte)(number1 >> 8), + (byte)(number1 >> 0), + (byte)(number2 >> 24), + (byte)(number2 >> 16), + (byte)(number2 >> 8), + (byte)(number2 >> 0), + (byte) key3[0], + (byte) key3[1], + (byte) key3[2], + (byte) key3[3], + (byte) key3[4], + (byte) key3[5], + (byte) key3[6], + (byte) key3[7] }; - - } - + + } + conn.socketChannel().write(ByteBuffer.wrap(clientHandshake.getHandshake())); - + } /** @@ -418,38 +418,38 @@ public void onClose(WebSocket conn) { } private String addNoise (String key) { - - Random rand = new Random(); - - for (int i = 0; i < (rand.nextInt(12) + 1); i++) { - // get a random non-numeric character - int x = 0; - while (x < 33 || ( x >= 48 && x <= 57)) { - x = (rand.nextInt(93) + 33); - } - char r = (char) x; - // get a random position in key - int pos = rand.nextInt(key.length()+1); - key = key.substring(0,pos) + r + key.substring(pos); - } - - return key; - + + Random rand = new Random(); + + for (int i = 0; i < (rand.nextInt(12) + 1); i++) { + // get a random non-numeric character + int x = 0; + while (x < 33 || ( x >= 48 && x <= 57)) { + x = (rand.nextInt(93) + 33); + } + char r = (char) x; + // get a random position in key + int pos = rand.nextInt(key.length()+1); + key = key.substring(0,pos) + r + key.substring(pos); + } + + return key; + } private String addSpaces (String key, int spaces) { - - Random rand = new Random(); - - for (int i = 0; i < spaces; i++) { - char space = (char) 32; - // get a random position in key that is not - int pos = rand.nextInt(key.length()-1) + 1; - key = key.substring(0,pos) + space + key.substring(pos); - } - - return key; - + + Random rand = new Random(); + + for (int i = 0; i < spaces; i++) { + char space = (char) 32; + // get a random position in key that is not + int pos = rand.nextInt(key.length()-1) + 1; + key = key.substring(0,pos) + space + key.substring(pos); + } + + return key; + } // ABTRACT METHODS ///////////////////////////////////////////////////////// diff --git a/src/WebSocketHandshake.java b/src/WebSocketHandshake.java index ce33e3bdf..be6f3a76f 100644 --- a/src/WebSocketHandshake.java +++ b/src/WebSocketHandshake.java @@ -10,275 +10,275 @@ */ public class WebSocketHandshake extends Hashtable implements WebSocketProtocol { - // INSTANCE PROPERTIES ///////////////////////////////////////////////////// - private static final long serialVersionUID = -280930290679993232L; - /** - * The raw handshake. - */ - private byte[] handshake; - /** - * The WebSocket Internet-Draft version. - */ - private Draft handshakeDraft; - /** - * The type of the handshake. The refers to the source of the handshake, either - * client or server. - */ - private ClientServerType handshakeType; - - // CONSTRUCTOR ///////////////////////////////////////////////////////////// - public WebSocketHandshake() { - super(); - } - - public WebSocketHandshake(byte[] handshake) { - super(); - setHandshake(handshake); - } - - public WebSocketHandshake(byte[] handshake, ClientServerType type, Draft draft) { - super(); - setDraft(draft); - setType(type); - setHandshake(handshake); - } - - // PUBLIC INSTANCE METHODS ///////////////////////////////////////////////// - public void setDraft(Draft handshakeDraft) { - this.handshakeDraft = handshakeDraft; - } - - public Draft getDraft() { - return handshakeDraft; - } - - public void setHandshake(byte[] handshake) { - this.handshake = handshake; - if (handshakeDraft != null && handshakeType != null) { - parseHandshake(); - } - } + // INSTANCE PROPERTIES ///////////////////////////////////////////////////// + private static final long serialVersionUID = -280930290679993232L; + /** + * The raw handshake. + */ + private byte[] handshake; + /** + * The WebSocket Internet-Draft version. + */ + private Draft handshakeDraft; + /** + * The type of the handshake. The refers to the source of the handshake, either + * client or server. + */ + private ClientServerType handshakeType; + + // CONSTRUCTOR ///////////////////////////////////////////////////////////// + public WebSocketHandshake() { + super(); + } + + public WebSocketHandshake(byte[] handshake) { + super(); + setHandshake(handshake); + } + + public WebSocketHandshake(byte[] handshake, ClientServerType type, Draft draft) { + super(); + setDraft(draft); + setType(type); + setHandshake(handshake); + } + + // PUBLIC INSTANCE METHODS ///////////////////////////////////////////////// + public void setDraft(Draft handshakeDraft) { + this.handshakeDraft = handshakeDraft; + } + + public Draft getDraft() { + return handshakeDraft; + } - public byte[] getHandshake() { - if (handshake == null && handshakeDraft != null && handshakeType != null) { - buildHandshake(); - } - return handshake; - } + public void setHandshake(byte[] handshake) { + this.handshake = handshake; + if (handshakeDraft != null && handshakeType != null) { + parseHandshake(); + } + } + + public byte[] getHandshake() { + if (handshake == null && handshakeDraft != null && handshakeType != null) { + buildHandshake(); + } + return handshake; + } + + public void setType(ClientServerType handshakeType) { + this.handshakeType = handshakeType; + } - public void setType(ClientServerType handshakeType) { - this.handshakeType = handshakeType; - } + public ClientServerType getType() { + return handshakeType; + } + + public String toString() { + return new String(getHandshake(),UTF8_CHARSET); + } + + public String getProperty(String fieldName) { + if (!containsKey(fieldName)) { + return null; + } + return (String) get(fieldName); + } - public ClientServerType getType() { - return handshakeType; - } - - public String toString() { - return new String(getHandshake(),UTF8_CHARSET); - } - - public String getProperty(String fieldName) { - if (!containsKey(fieldName)) { - return null; - } - return (String) get(fieldName); - } - - public byte[] getAsByteArray(String fieldName) { - if (!containsKey(fieldName)) { - return null; - } - return (byte[]) get(fieldName); - } - - /** - * Parses the parts of the handshake into Hashtable keys. Can only be - * called if the handshake draft and type are set. Called automatically - * when handshake is set, if the draft and type are known and set for - * the handshake. - */ - public void parseHandshake() { - - if (this.handshakeDraft == null) throw new NullPointerException("Handshake draft type must be set before parsing."); - if (this.handshakeType == null) throw new NullPointerException("Handshake type type must be set before parsing."); - - // parse the HTTP request-line or HTTP status-line - - String hs = this.toString(); - String[] requestLines = hs.split("\r\n"); - String line = requestLines[0].trim(); - - if (this.handshakeType == ClientServerType.SERVER) { - int sp1 = line.indexOf(" "); - int sp2 = line.indexOf(" ",sp1+1); - String httpVersion = line.substring(0,sp1); - String statusCode = line.substring(sp1+1,sp2); - String reasonPhrase = line.substring(sp2+1,line.length()); - put("http-version", httpVersion); - put("status-code", statusCode); - put("reason-phrase", reasonPhrase); - } - - if (this.handshakeType == ClientServerType.CLIENT) { - int sp1 = line.indexOf(" "); - int sp2 = line.indexOf(" ",sp1+1); - String method = line.substring(0,sp1); - String requestURI = line.substring(sp1+1,sp2); - String httpVersion = line.substring(sp2+1,line.length()); - put("method", method); - put("request-uri", requestURI); - put("http-version", httpVersion); - } - - // parse fields - - for (int i=1; idraft and type are known and set for + * the handshake. + */ + public void parseHandshake() { + + if (this.handshakeDraft == null) throw new NullPointerException("Handshake draft type must be set before parsing."); + if (this.handshakeType == null) throw new NullPointerException("Handshake type type must be set before parsing."); + + // parse the HTTP request-line or HTTP status-line + + String hs = this.toString(); + String[] requestLines = hs.split("\r\n"); + String line = requestLines[0].trim(); + + if (this.handshakeType == ClientServerType.SERVER) { + int sp1 = line.indexOf(" "); + int sp2 = line.indexOf(" ",sp1+1); + String httpVersion = line.substring(0,sp1); + String statusCode = line.substring(sp1+1,sp2); + String reasonPhrase = line.substring(sp2+1,line.length()); + put("http-version", httpVersion); + put("status-code", statusCode); + put("reason-phrase", reasonPhrase); + } + + if (this.handshakeType == ClientServerType.CLIENT) { + int sp1 = line.indexOf(" "); + int sp2 = line.indexOf(" ",sp1+1); + String method = line.substring(0,sp1); + String requestURI = line.substring(sp1+1,sp2); + String httpVersion = line.substring(sp2+1,line.length()); + put("method", method); + put("request-uri", requestURI); + put("http-version", httpVersion); + } + + // parse fields + + for (int i=1; idraft and type as well as the keys - * set for this handshake. Called automatically - * when handshake is requested, if the draft and type are known and set for - * the handshake. - */ - public void buildHandshake() { - - if (this.handshakeDraft == null) throw new NullPointerException("Handshake draft type must be set before building."); - if (this.handshakeType == null) throw new NullPointerException("Handshake type type must be set before building."); - - if (this.handshakeType == ClientServerType.SERVER) { - - String responseHandshake = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" + - "Upgrade: WebSocket\r\n" + - "Connection: Upgrade\r\n"; - - if (this.handshakeDraft == Draft.DRAFT75) { - responseHandshake += "WebSocket-Origin: " + getProperty("origin") + "\r\n" + - "WebSocket-Location: ws://" + getProperty("host") + getProperty("request-uri") + "\r\n"; - if (containsKey("WebSocket-Protocol")) { - responseHandshake += "WebSocket-Protocol: " + getProperty("websocket-protocol") + "\r\n"; - } - } - - if (this.handshakeDraft == Draft.DRAFT76) { - responseHandshake += "Sec-WebSocket-Location: ws://" + getProperty("host") + getProperty("request-uri") + "\r\n" + - "Sec-WebSocket-Origin: " + getProperty("origin") + "\r\n"; - if (containsKey("sec-websocket-protocol")) { - responseHandshake += "Sec-WebSocket-Protocol: " + getProperty("sec-websocket-protocol") + "\r\n"; - } - - } - - responseHandshake += "\r\n"; - - byte[] responseHandshakeBytes = responseHandshake.getBytes(UTF8_CHARSET); - - try{ - ByteArrayOutputStream buff = new ByteArrayOutputStream(); - buff.write(responseHandshakeBytes); - if (this.handshakeDraft == Draft.DRAFT76) { - byte[] response = getAsByteArray("response"); - buff.write(response); - } - this.handshake = buff.toByteArray(); - return; - } catch(IOException e) { - System.out.print(e); - }; - - } - - if (this.handshakeType == ClientServerType.CLIENT) { - - String requestHandshake = "GET " + getProperty("request-uri") + " HTTP/1.1\r\n" + - "Upgrade: WebSocket\r\n" + - "Connection: Upgrade\r\n" + - "Host: " + getProperty("host") + "\r\n" + - "Origin: " + getProperty("origin") + "\r\n"; - - if (getDraft() == Draft.DRAFT75 && containsKey("websocket-protocol")) { - requestHandshake += "WebSocket-Protocol: " + getProperty("websocket-protocol") + "\r\n"; - } - - if (getDraft() == Draft.DRAFT76 ) { - if(containsKey("sec-webSocket-protocol")) { - requestHandshake += "Sec-WebSocket-Protocol: " + getProperty("sec-websocket-protocol") + "\r\n"; - } - requestHandshake += "Sec-WebSocket-Key1: " + getProperty("sec-websocket-key1") + "\r\n"; - requestHandshake += "Sec-WebSocket-Key2: " + getProperty("sec-websocket-key2") + "\r\n"; - } - - requestHandshake += "\r\n"; - - byte[] requestHandshakeBytes = requestHandshake.getBytes(UTF8_CHARSET); - - try{ - ByteArrayOutputStream buff = new ByteArrayOutputStream(); - buff.write(requestHandshakeBytes); - if (this.handshakeDraft == Draft.DRAFT76) { - byte[] key3 = getAsByteArray("key3"); - buff.write(key3); - } - this.handshake = buff.toByteArray(); - return; - } catch(IOException e) { - System.out.print(e); - }; - - } - - } + + // get the key3 bytes + + if (this.handshakeDraft == Draft.DRAFT76) { + int l = getHandshake().length; + if (this.handshakeType == ClientServerType.CLIENT){ + //get last 8 bytes + byte[] key3 = { + getHandshake()[l-8], + getHandshake()[l-7], + getHandshake()[l-6], + getHandshake()[l-5], + getHandshake()[l-4], + getHandshake()[l-3], + getHandshake()[l-2], + getHandshake()[l-1] + }; + put("key3", key3); + } + if (this.handshakeType == ClientServerType.SERVER){ + //get last 16 bytes + byte[] response = { + getHandshake()[l-16], + getHandshake()[l-15], + getHandshake()[l-14], + getHandshake()[l-13], + getHandshake()[l-12], + getHandshake()[l-11], + getHandshake()[l-10], + getHandshake()[l-9], + getHandshake()[l-8], + getHandshake()[l-7], + getHandshake()[l-6], + getHandshake()[l-5], + getHandshake()[l-4], + getHandshake()[l-3], + getHandshake()[l-2], + getHandshake()[l-1] + }; + put("response", response); + } + } + } + + /** + * Generates the handshake byte array based on the handshake draft and type as well as the keys + * set for this handshake. Called automatically + * when handshake is requested, if the draft and type are known and set for + * the handshake. + */ + public void buildHandshake() { + + if (this.handshakeDraft == null) throw new NullPointerException("Handshake draft type must be set before building."); + if (this.handshakeType == null) throw new NullPointerException("Handshake type type must be set before building."); + + if (this.handshakeType == ClientServerType.SERVER) { + + String responseHandshake = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" + + "Upgrade: WebSocket\r\n" + + "Connection: Upgrade\r\n"; + + if (this.handshakeDraft == Draft.DRAFT75) { + responseHandshake += "WebSocket-Origin: " + getProperty("origin") + "\r\n" + + "WebSocket-Location: ws://" + getProperty("host") + getProperty("request-uri") + "\r\n"; + if (containsKey("WebSocket-Protocol")) { + responseHandshake += "WebSocket-Protocol: " + getProperty("websocket-protocol") + "\r\n"; + } + } + + if (this.handshakeDraft == Draft.DRAFT76) { + responseHandshake += "Sec-WebSocket-Location: ws://" + getProperty("host") + getProperty("request-uri") + "\r\n" + + "Sec-WebSocket-Origin: " + getProperty("origin") + "\r\n"; + if (containsKey("sec-websocket-protocol")) { + responseHandshake += "Sec-WebSocket-Protocol: " + getProperty("sec-websocket-protocol") + "\r\n"; + } + + } + + responseHandshake += "\r\n"; + + byte[] responseHandshakeBytes = responseHandshake.getBytes(UTF8_CHARSET); + + try{ + ByteArrayOutputStream buff = new ByteArrayOutputStream(); + buff.write(responseHandshakeBytes); + if (this.handshakeDraft == Draft.DRAFT76) { + byte[] response = getAsByteArray("response"); + buff.write(response); + } + this.handshake = buff.toByteArray(); + return; + } catch(IOException e) { + System.out.print(e); + }; + + } + + if (this.handshakeType == ClientServerType.CLIENT) { + + String requestHandshake = "GET " + getProperty("request-uri") + " HTTP/1.1\r\n" + + "Upgrade: WebSocket\r\n" + + "Connection: Upgrade\r\n" + + "Host: " + getProperty("host") + "\r\n" + + "Origin: " + getProperty("origin") + "\r\n"; + + if (getDraft() == Draft.DRAFT75 && containsKey("websocket-protocol")) { + requestHandshake += "WebSocket-Protocol: " + getProperty("websocket-protocol") + "\r\n"; + } + + if (getDraft() == Draft.DRAFT76 ) { + if(containsKey("sec-webSocket-protocol")) { + requestHandshake += "Sec-WebSocket-Protocol: " + getProperty("sec-websocket-protocol") + "\r\n"; + } + requestHandshake += "Sec-WebSocket-Key1: " + getProperty("sec-websocket-key1") + "\r\n"; + requestHandshake += "Sec-WebSocket-Key2: " + getProperty("sec-websocket-key2") + "\r\n"; + } + + requestHandshake += "\r\n"; + + byte[] requestHandshakeBytes = requestHandshake.getBytes(UTF8_CHARSET); + + try{ + ByteArrayOutputStream buff = new ByteArrayOutputStream(); + buff.write(requestHandshakeBytes); + if (this.handshakeDraft == Draft.DRAFT76) { + byte[] key3 = getAsByteArray("key3"); + buff.write(key3); + } + this.handshake = buff.toByteArray(); + return; + } catch(IOException e) { + System.out.print(e); + }; + + } + + } } diff --git a/src/WebSocketProtocol.java b/src/WebSocketProtocol.java index 4b492ebde..e7f48e124 100644 --- a/src/WebSocketProtocol.java +++ b/src/WebSocketProtocol.java @@ -6,31 +6,31 @@ * @author Nathan Mische */ interface WebSocketProtocol { - - /** - * The version of the WebSocket Internet-Draft - */ - public enum Draft { - AUTO, - DRAFT75, - DRAFT76 - } - - /** - * The maximum value of a WebSocket key. - */ - public static final Long MAX_KEY_VALUE = Long.parseLong("4294967295"); - - /** + + /** + * The version of the WebSocket Internet-Draft + */ + public enum Draft { + AUTO, + DRAFT75, + DRAFT76 + } + + /** + * The maximum value of a WebSocket key. + */ + public static final Long MAX_KEY_VALUE = Long.parseLong("4294967295"); + + /** * The WebSocket protocol expects UTF-8 encoded bytes. */ public static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); /** - * The type of WebSocket - */ - public enum ClientServerType { - CLIENT, - SERVER - } + * The type of WebSocket + */ + public enum ClientServerType { + CLIENT, + SERVER + } } diff --git a/src/WebSocketServer.java b/src/WebSocketServer.java index 9d76a47d3..598390c7e 100644 --- a/src/WebSocketServer.java +++ b/src/WebSocketServer.java @@ -79,14 +79,14 @@ public abstract class WebSocketServer implements Runnable, WebSocketListener { public WebSocketServer() { this(DEFAULT_PORT,null,null,DEFAULT_DRAFT); } - + /** * Creates a WebSocketServer that will attempt to listen on port * port. * @param port The port number this server should listen on. */ public WebSocketServer(int port) { - this(port,null,null,DEFAULT_DRAFT); + this(port,null,null,DEFAULT_DRAFT); } /** @@ -96,7 +96,7 @@ public WebSocketServer(int port) { * @param origin The origin this server supports. */ public WebSocketServer(int port, String origin) { - this(port,origin,null,DEFAULT_DRAFT); + this(port,origin,null,DEFAULT_DRAFT); } /** @@ -108,7 +108,7 @@ public WebSocketServer(int port, String origin) { * @param subprotocol The subprotocol this server supports. */ public WebSocketServer(int port, String origin, String subprotocol) { - this(port,origin,subprotocol,DEFAULT_DRAFT); + this(port,origin,subprotocol,DEFAULT_DRAFT); } /** @@ -122,7 +122,7 @@ public WebSocketServer(int port, String origin, String subprotocol) { * @param draft The draft of the WebSocket protocol this server supports. */ public WebSocketServer(int port, String origin, String subprotocol, String draft) { - this(port,origin,subprotocol,Draft.valueOf(draft)); + this(port,origin,subprotocol,Draft.valueOf(draft)); } /** @@ -136,13 +136,13 @@ public WebSocketServer(int port, String origin, String subprotocol, String draft * @param draft The draft of the WebSocket protocol this server supports. */ public WebSocketServer(int port, String origin, String subprotocol, Draft draft) { - this.connections = new CopyOnWriteArraySet(); + this.connections = new CopyOnWriteArraySet(); setPort(port); setOrigin(origin); setSubProtocol(subprotocol); setDraft(draft); } - + // PUBLIC INSTANCE METHODS ///////////////////////////////////////////////// /** @@ -152,7 +152,7 @@ public WebSocketServer(int port, String origin, String subprotocol, Draft draft) public void setDraft(Draft draft) { this.draft = draft; } - + /** * Gets the draft that this WebSocketClient supports. * @return The draft for this WebSocketClient. @@ -169,7 +169,7 @@ public Draft getDraft() { public void setOrigin(String origin) { this.origin = origin; } - + /** * Gets the origin that this WebSocketServer should allow connections * from. @@ -186,7 +186,7 @@ public String getOrigin() { public void setPort(int port) { this.port = port; } - + /** * Gets the port number that this server listens on. * @return The port number. @@ -194,7 +194,7 @@ public void setPort(int port) { public int getPort() { return port; } - + /** * Sets this WebSocketClient subprotocol. * @param subprotocol @@ -202,7 +202,7 @@ public int getPort() { public void setSubProtocol(String subprotocol) { this.subprotocol = subprotocol; } - + /** * Gets the subprotocol that this WebSocketClient supports. * @return The subprotocol for this WebSocketClient. @@ -210,7 +210,6 @@ public void setSubProtocol(String subprotocol) { public String getSubProtocol() { return subprotocol; } - /** * Starts the server thread that binds to the currently set port number and @@ -278,7 +277,7 @@ public WebSocket[] connections() { return this.connections.toArray(new WebSocket[0]); } - + // Runnable IMPLEMENTATION ///////////////////////////////////////////////// public void run() { try { @@ -368,16 +367,16 @@ public boolean onHandshakeRecieved(WebSocket conn, WebSocketHandshake handshake) return handleHandshake76(conn,handshake); } - // If we get here we got a handshake that is incompatibile with the server. - return false; + // If we get here we got a handshake that is incompatibile with the server. + return false; } private boolean handleHandshake75(WebSocket conn, WebSocketHandshake handshake) throws IOException { - + if (!handshake.getProperty("method").equals("GET") - || !handshake.getProperty("http-version").equals("HTTP/1.1") - || !handshake.getProperty("request-uri").startsWith("/")) { - return false; + || !handshake.getProperty("http-version").equals("HTTP/1.1") + || !handshake.getProperty("request-uri").startsWith("/")) { + return false; } String prop = handshake.getProperty("Upgrade"); @@ -391,19 +390,19 @@ private boolean handleHandshake75(WebSocket conn, WebSocketHandshake handshake) } if (subprotocol != null) { - prop = handshake.getProperty("Websocket-Protocol"); + prop = handshake.getProperty("Websocket-Protocol"); if (prop == null || !prop.equals(subprotocol)) { return false; - } + } } if (origin != null) { - prop = handshake.getProperty("Origin"); + prop = handshake.getProperty("Origin"); if (prop == null || !prop.startsWith(origin)) { return false; } - } - + } + // If we've determined that this is a valid WebSocket request, send a // valid WebSocket server handshake, then return true to keep connection alive. @@ -414,9 +413,9 @@ private boolean handleHandshake75(WebSocket conn, WebSocketHandshake handshake) serverHandshake.put("request-uri", handshake.getProperty("request-uri")); serverHandshake.put("origin", handshake.getProperty("Origin")); if (handshake.containsKey("Websocket-Protocol")) { - serverHandshake.put("websocket-protocol", handshake.getProperty("Websocket-Protocol")); + serverHandshake.put("websocket-protocol", handshake.getProperty("Websocket-Protocol")); } - + conn.socketChannel().write(ByteBuffer.wrap(serverHandshake.getHandshake())); return true; @@ -424,105 +423,105 @@ private boolean handleHandshake75(WebSocket conn, WebSocketHandshake handshake) } private boolean handleHandshake76(WebSocket conn, WebSocketHandshake handshake) throws IOException { - + if (!handshake.getProperty("method").equals("GET") - || !handshake.getProperty("request-uri").matches("^/[\u0021-\u007E]*")) { - return false; + || !handshake.getProperty("request-uri").matches("^/[\u0021-\u007E]*")) { + return false; } String prop; prop = handshake.getProperty("upgrade"); if (prop == null || !(prop.equalsIgnoreCase("WebSocket"))) { - return false; - } + return false; + } prop = handshake.getProperty("connection"); if (prop == null || !(prop.equalsIgnoreCase("Upgrade"))) { - return false; + return false; } - + if (!handshake.containsKey("host")) { - return false; + return false; } - - + + if (!handshake.containsKey("origin")) { - return false; + return false; } if (origin != null) { - prop = handshake.getProperty("origin"); + prop = handshake.getProperty("origin"); if (prop == null || !prop.startsWith(origin)) { return false; } } - + if (subprotocol != null) { - prop = handshake.getProperty("sec-websocket-protocol"); + prop = handshake.getProperty("sec-websocket-protocol"); if (prop == null || !prop.equals(subprotocol)) { return false; - } + } } - + if (!handshake.containsKey("sec-websocket-key1") || !handshake.containsKey("sec-websocket-key2")) { - return false; + return false; } - + byte[] key3 = handshake.getAsByteArray("key3"); - + long key1 = Long.parseLong(handshake.getProperty("sec-websocket-key1").replaceAll("[^0-9]","")); long key2 = Long.parseLong(handshake.getProperty("sec-websocket-key2").replaceAll("[^0-9]","")); if (key1 > MAX_KEY_VALUE || key2 > MAX_KEY_VALUE) { - return false; + return false; } - + int spaces1 = handshake.getProperty("sec-websocket-key1").replaceAll("[^ ]", "").length(); int spaces2 = handshake.getProperty("sec-websocket-key2").replaceAll("[^ ]", "").length(); if (spaces1 == 0 || spaces2 == 0) { - return false; + return false; } - + long mod1 = key1 % spaces1; long mod2 = key2 % spaces2; if (mod1 != 0 || mod2 != 0) { - return false; + return false; } - + long part1 = key1/spaces1; long part2 = key2/spaces2; byte[] challenge = { - (byte)(part1 >> 24), - (byte)(part1 >> 16), - (byte)(part1 >> 8), - (byte)(part1 >> 0), - (byte)(part2 >> 24), - (byte)(part2 >> 16), - (byte)(part2 >> 8), - (byte)(part2 >> 0), - (byte) key3[0], - (byte) key3[1], - (byte) key3[2], - (byte) key3[3], - (byte) key3[4], - (byte) key3[5], - (byte) key3[6], - (byte) key3[7] + (byte)(part1 >> 24), + (byte)(part1 >> 16), + (byte)(part1 >> 8), + (byte)(part1 >> 0), + (byte)(part2 >> 24), + (byte)(part2 >> 16), + (byte)(part2 >> 8), + (byte)(part2 >> 0), + (byte) key3[0], + (byte) key3[1], + (byte) key3[2], + (byte) key3[3], + (byte) key3[4], + (byte) key3[5], + (byte) key3[6], + (byte) key3[7] }; byte[] response; try { - MessageDigest md = MessageDigest.getInstance("MD5"); - response = md.digest(challenge); + MessageDigest md = MessageDigest.getInstance("MD5"); + response = md.digest(challenge); } catch (NoSuchAlgorithmException e) { - return false; + return false; } - + WebSocketHandshake serverHandshake = new WebSocketHandshake(); serverHandshake.setType(ClientServerType.SERVER); serverHandshake.setDraft(Draft.DRAFT76); @@ -531,12 +530,12 @@ private boolean handleHandshake76(WebSocket conn, WebSocketHandshake handshake) serverHandshake.put("origin", handshake.getProperty("origin")); serverHandshake.put("response", response); if (handshake.containsKey("sec-websocket-protocol")) { - serverHandshake.put("sec-websocket-protocol", handshake.getProperty("sec-websocket-protocol")); + serverHandshake.put("sec-websocket-protocol", handshake.getProperty("sec-websocket-protocol")); } - + conn.socketChannel().write(ByteBuffer.wrap(serverHandshake.getHandshake())); - - return true; + + return true; } @@ -558,5 +557,5 @@ public void onClose(WebSocket conn) { public abstract void onClientOpen(WebSocket conn); public abstract void onClientClose(WebSocket conn); public abstract void onClientMessage(WebSocket conn, String message); - + } From e2a6af8b8c0c680385e444105aae4c03e2629e4b Mon Sep 17 00:00:00 2001 From: Nathan Mische Date: Mon, 26 Jul 2010 16:14:29 -0400 Subject: [PATCH 08/12] Fixing field case. Draft 75 is case-sensitive while Draft 76 is case-insensitive. --- src/WebSocketClient.java | 19 +++++++++---- src/WebSocketHandshake.java | 55 +++++++++++++++++++++++++++---------- src/WebSocketServer.java | 14 +++++----- 3 files changed, 61 insertions(+), 27 deletions(-) diff --git a/src/WebSocketClient.java b/src/WebSocketClient.java index f6e0d0011..56f8c122f 100644 --- a/src/WebSocketClient.java +++ b/src/WebSocketClient.java @@ -320,14 +320,23 @@ private void sendClientHandshake(WebSocket conn) throws IOException { WebSocketHandshake clientHandshake = new WebSocketHandshake(); clientHandshake.setType(ClientServerType.CLIENT); clientHandshake.setDraft(getDraft()); - clientHandshake.put("request-uri", requestURI); - clientHandshake.put("host", host); - clientHandshake.put("origin", origin); + + if (getDraft() == Draft.DRAFT75) { + clientHandshake.put("Request-Uri", requestURI); + clientHandshake.put("Host", host); + clientHandshake.put("Origin", origin); + } else { + clientHandshake.put("request-uri", requestURI); + clientHandshake.put("host", host); + clientHandshake.put("origin", origin); + } + + if (subprotocol != null) { if (getDraft() == Draft.DRAFT75) { - clientHandshake.put("websocket-protocol", subprotocol); + clientHandshake.put("Websocket-Protocol", subprotocol); } else { - clientHandshake.put("sec-webSocket-protocol", subprotocol); + clientHandshake.put("sec-websocket-protocol", subprotocol); } } diff --git a/src/WebSocketHandshake.java b/src/WebSocketHandshake.java index be6f3a76f..cb11a0c00 100644 --- a/src/WebSocketHandshake.java +++ b/src/WebSocketHandshake.java @@ -112,12 +112,22 @@ public void parseHandshake() { if (this.handshakeType == ClientServerType.SERVER) { int sp1 = line.indexOf(" "); int sp2 = line.indexOf(" ",sp1+1); - String httpVersion = line.substring(0,sp1); + String httpVersion = line.substring(0,sp1); String statusCode = line.substring(sp1+1,sp2); String reasonPhrase = line.substring(sp2+1,line.length()); - put("http-version", httpVersion); - put("status-code", statusCode); - put("reason-phrase", reasonPhrase); + String httpVersionKey = "HTTP-Version"; + String statusCodeKey = "Status-Code"; + String reasonPhraseKey = "Reason-Phrase"; + + if (this.handshakeDraft == Draft.DRAFT76) { + httpVersionKey = httpVersionKey.toLowerCase(); + statusCodeKey = statusCodeKey.toLowerCase(); + reasonPhraseKey = reasonPhraseKey.toLowerCase(); + } + + put(httpVersionKey, httpVersion); + put(statusCodeKey, statusCode); + put(reasonPhraseKey, reasonPhrase); } if (this.handshakeType == ClientServerType.CLIENT) { @@ -126,9 +136,20 @@ public void parseHandshake() { String method = line.substring(0,sp1); String requestURI = line.substring(sp1+1,sp2); String httpVersion = line.substring(sp2+1,line.length()); - put("method", method); - put("request-uri", requestURI); - put("http-version", httpVersion); + String methodKey = "Method"; + String requestURIKey = "Request-URI"; + String httpVersionKey = "HTTP-Version"; + + + if (this.handshakeDraft == Draft.DRAFT76) { + methodKey = methodKey.toLowerCase(); + requestURIKey = httpVersionKey.toLowerCase(); + httpVersionKey = httpVersionKey.toLowerCase(); + } + + put(methodKey, method); + put(requestURIKey, requestURI); + put(httpVersionKey, httpVersion); } // parse fields @@ -206,10 +227,10 @@ public void buildHandshake() { "Connection: Upgrade\r\n"; if (this.handshakeDraft == Draft.DRAFT75) { - responseHandshake += "WebSocket-Origin: " + getProperty("origin") + "\r\n" + - "WebSocket-Location: ws://" + getProperty("host") + getProperty("request-uri") + "\r\n"; + responseHandshake += "WebSocket-Origin: " + getProperty("Origin") + "\r\n" + + "WebSocket-Location: ws://" + getProperty("Host") + getProperty("Request-URI") + "\r\n"; if (containsKey("WebSocket-Protocol")) { - responseHandshake += "WebSocket-Protocol: " + getProperty("websocket-protocol") + "\r\n"; + responseHandshake += "WebSocket-Protocol: " + getProperty("Websocket-Protocol") + "\r\n"; } } @@ -243,14 +264,18 @@ public void buildHandshake() { if (this.handshakeType == ClientServerType.CLIENT) { - String requestHandshake = "GET " + getProperty("request-uri") + " HTTP/1.1\r\n" + + String requestURI = (getDraft() == Draft.DRAFT75) ? getProperty("Request-URI") : getProperty("request-uri"); + String host = (getDraft() == Draft.DRAFT75) ? getProperty("Host") : getProperty("host"); + String origin = (getDraft() == Draft.DRAFT75) ? getProperty("Origin") : getProperty("origin"); + + String requestHandshake = "GET " + requestURI + " HTTP/1.1\r\n" + "Upgrade: WebSocket\r\n" + "Connection: Upgrade\r\n" + - "Host: " + getProperty("host") + "\r\n" + - "Origin: " + getProperty("origin") + "\r\n"; + "Host: " + host + "\r\n" + + "Origin: " + origin + "\r\n"; - if (getDraft() == Draft.DRAFT75 && containsKey("websocket-protocol")) { - requestHandshake += "WebSocket-Protocol: " + getProperty("websocket-protocol") + "\r\n"; + if (getDraft() == Draft.DRAFT75 && containsKey("Websocket-Protocol")) { + requestHandshake += "WebSocket-Protocol: " + getProperty("Websocket-Protocol") + "\r\n"; } if (getDraft() == Draft.DRAFT76 ) { diff --git a/src/WebSocketServer.java b/src/WebSocketServer.java index 598390c7e..64906fc9a 100644 --- a/src/WebSocketServer.java +++ b/src/WebSocketServer.java @@ -373,9 +373,9 @@ public boolean onHandshakeRecieved(WebSocket conn, WebSocketHandshake handshake) private boolean handleHandshake75(WebSocket conn, WebSocketHandshake handshake) throws IOException { - if (!handshake.getProperty("method").equals("GET") - || !handshake.getProperty("http-version").equals("HTTP/1.1") - || !handshake.getProperty("request-uri").startsWith("/")) { + if (!handshake.getProperty("Method").equals("GET") + || !handshake.getProperty("HTTP-Version").equals("HTTP/1.1") + || !handshake.getProperty("Request-URI").startsWith("/")) { return false; } @@ -409,11 +409,11 @@ private boolean handleHandshake75(WebSocket conn, WebSocketHandshake handshake) WebSocketHandshake serverHandshake = new WebSocketHandshake(); serverHandshake.setType(ClientServerType.SERVER); serverHandshake.setDraft(Draft.DRAFT75); - serverHandshake.put("host", handshake.getProperty("Host")); - serverHandshake.put("request-uri", handshake.getProperty("request-uri")); - serverHandshake.put("origin", handshake.getProperty("Origin")); + serverHandshake.put("Host", handshake.getProperty("Host")); + serverHandshake.put("Request-Uri", handshake.getProperty("request-uri")); + serverHandshake.put("Origin", handshake.getProperty("Origin")); if (handshake.containsKey("Websocket-Protocol")) { - serverHandshake.put("websocket-protocol", handshake.getProperty("Websocket-Protocol")); + serverHandshake.put("Websocket-Protocol", handshake.getProperty("Websocket-Protocol")); } conn.socketChannel().write(ByteBuffer.wrap(serverHandshake.getHandshake())); From 0d5e5688a240914db27d7362707fa079e6f2d78a Mon Sep 17 00:00:00 2001 From: Nathan Mische Date: Mon, 26 Jul 2010 21:04:11 -0400 Subject: [PATCH 09/12] A few more minor fixes. --- src/WebSocketHandshake.java | 5 ++--- src/WebSocketServer.java | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/WebSocketHandshake.java b/src/WebSocketHandshake.java index cb11a0c00..ecf220cb5 100644 --- a/src/WebSocketHandshake.java +++ b/src/WebSocketHandshake.java @@ -103,7 +103,7 @@ public void parseHandshake() { if (this.handshakeDraft == null) throw new NullPointerException("Handshake draft type must be set before parsing."); if (this.handshakeType == null) throw new NullPointerException("Handshake type type must be set before parsing."); - // parse the HTTP request-line or HTTP status-line + // parse the HTTP request-line or HTTP status-line String hs = this.toString(); String[] requestLines = hs.split("\r\n"); @@ -140,10 +140,9 @@ public void parseHandshake() { String requestURIKey = "Request-URI"; String httpVersionKey = "HTTP-Version"; - if (this.handshakeDraft == Draft.DRAFT76) { methodKey = methodKey.toLowerCase(); - requestURIKey = httpVersionKey.toLowerCase(); + requestURIKey = requestURIKey.toLowerCase(); httpVersionKey = httpVersionKey.toLowerCase(); } diff --git a/src/WebSocketServer.java b/src/WebSocketServer.java index 64906fc9a..4c7b49d67 100644 --- a/src/WebSocketServer.java +++ b/src/WebSocketServer.java @@ -410,7 +410,7 @@ private boolean handleHandshake75(WebSocket conn, WebSocketHandshake handshake) serverHandshake.setType(ClientServerType.SERVER); serverHandshake.setDraft(Draft.DRAFT75); serverHandshake.put("Host", handshake.getProperty("Host")); - serverHandshake.put("Request-Uri", handshake.getProperty("request-uri")); + serverHandshake.put("Request-URI", handshake.getProperty("Request-URI")); serverHandshake.put("Origin", handshake.getProperty("Origin")); if (handshake.containsKey("Websocket-Protocol")) { serverHandshake.put("Websocket-Protocol", handshake.getProperty("Websocket-Protocol")); @@ -455,13 +455,13 @@ private boolean handleHandshake76(WebSocket conn, WebSocketHandshake handshake) if (prop == null || !prop.startsWith(origin)) { return false; } - } + } if (subprotocol != null) { prop = handshake.getProperty("sec-websocket-protocol"); if (prop == null || !prop.equals(subprotocol)) { return false; - } + } } if (!handshake.containsKey("sec-websocket-key1") || !handshake.containsKey("sec-websocket-key2")) { From 5260e1237572b13c8fe0631cdfc1d924007e91c1 Mon Sep 17 00:00:00 2001 From: Nathan Mische Date: Mon, 20 Sep 2010 23:53:55 -0400 Subject: [PATCH 10/12] Fix to asynch I/O --- src/WebSocket.java | 509 +++++++++------- src/WebSocketClient.java | 876 ++++++++++++++------------- src/WebSocketHandshake.java | 571 ++++++++--------- src/WebSocketListener.java | 86 +-- src/WebSocketProtocol.java | 53 +- src/WebSocketServer.java | 1144 ++++++++++++++++++++--------------- 6 files changed, 1766 insertions(+), 1473 deletions(-) diff --git a/src/WebSocket.java b/src/WebSocket.java index f26649e93..f23aac429 100644 --- a/src/WebSocket.java +++ b/src/WebSocket.java @@ -4,247 +4,324 @@ import java.nio.ByteBuffer; import java.nio.channels.NotYetConnectedException; import java.nio.channels.SocketChannel; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.BlockingQueue; /** - * Represents one end (client or server) of a single WebSocket connection. - * Takes care of the "handshake" phase, then allows for easy sending of - * text frames, and recieving frames through an event-based model. - * + * Represents one end (client or server) of a single WebSocket connection. Takes + * care of the "handshake" phase, then allows for easy sending of text frames, + * and recieving frames through an event-based model. + * * This is an inner class, used by WebSocketClient and * WebSocketServer, and should never need to be instantiated directly * by your code. + * * @author Nathan Rajlich */ public final class WebSocket implements WebSocketProtocol { - // CONSTANTS /////////////////////////////////////////////////////////////// - /** - * The byte representing CR, or Carriage Return, or \r - */ - public static final byte CR = (byte)0x0D; - /** - * The byte representing LF, or Line Feed, or \n - */ - public static final byte LF = (byte)0x0A; - /** - * The byte representing the beginning of a WebSocket text frame. - */ - public static final byte START_OF_FRAME = (byte)0x00; - /** - * The byte representing the end of a WebSocket text frame. - */ - public static final byte END_OF_FRAME = (byte)0xFF; - - // INSTANCE PROPERTIES ///////////////////////////////////////////////////// - /** - * The SocketChannel instance to use for this server connection. - * This is used to read and write data to. - */ - private final SocketChannel socketChannel; - /** - * Internally used to determine whether to recieve data as part of the - * remote handshake, or as part of a text frame. - */ - private boolean handshakeComplete; - /** - * The listener to notify of WebSocket events. - */ - private WebSocketListener wsl; - /** - * The 1-byte buffer reused throughout the WebSocket connection to read data. - */ - private ByteBuffer buffer; - /** - * The bytes that make up the remote handshake. - */ - private ByteBuffer remoteHandshakeBuffer; - /** - * The bytes that make up the current text frame being read. - */ - private ByteBuffer currentFrame; - /** - * The type of WebSocket. - */ - private ClientServerType wstype; - - // CONSTRUCTOR ///////////////////////////////////////////////////////////// - /** - * Used in {@link WebSocketServer} and {@link WebSocketClient}. - * @param socketChannel The SocketChannel instance to read and - * write to. The channel should already be registered - * with a Selector before construction of this object. - * @param listener The {@link WebSocketListener} to notify of events when - * they occur. - * @param wstype The type of WebSocket, client or server. - */ - public WebSocket(SocketChannel socketChannel, WebSocketListener listener, ClientServerType wstype) { - this.socketChannel = socketChannel; - this.handshakeComplete = false; - this.remoteHandshakeBuffer = this.currentFrame = null; - this.buffer = ByteBuffer.allocate(1); - this.wsl = listener; - this.wstype = wstype; + // CONSTANTS /////////////////////////////////////////////////////////////// + /** + * The byte representing CR, or Carriage Return, or \r + */ + public static final byte CR = (byte) 0x0D; + /** + * The byte representing LF, or Line Feed, or \n + */ + public static final byte LF = (byte) 0x0A; + /** + * The byte representing the beginning of a WebSocket text frame. + */ + public static final byte START_OF_FRAME = (byte) 0x00; + /** + * The byte representing the end of a WebSocket text frame. + */ + public static final byte END_OF_FRAME = (byte) 0xFF; + + // INSTANCE PROPERTIES ///////////////////////////////////////////////////// + /** + * The SocketChannel instance to use for this server connection. This + * is used to read and write data to. + */ + private final SocketChannel socketChannel; + /** + * Internally used to determine whether to recieve data as part of the remote + * handshake, or as part of a text frame. + */ + private boolean handshakeComplete; + /** + * The listener to notify of WebSocket events. + */ + private WebSocketListener wsl; + /** + * The 1-byte buffer reused throughout the WebSocket connection to read data. + */ + private ByteBuffer buffer; + /** + * The bytes that make up the remote handshake. + */ + private ByteBuffer remoteHandshakeBuffer; + /** + * The bytes that make up the current text frame being read. + */ + private ByteBuffer currentFrame; + /** + * Queue of buffers that need to be sent to the client. + */ + private BlockingQueue bufferQueue; + /** + * Lock object to ensure that data is sent from the bufferQueue in the proper + * order + */ + private Object bufferQueueMutex = new Object(); + /** + * The type of WebSocket. + */ + private ClientServerType wstype; + + // CONSTRUCTOR ///////////////////////////////////////////////////////////// + /** + * Used in {@link WebSocketServer} and {@link WebSocketClient}. + * + * @param socketChannel + * The SocketChannel instance to read and write to. The + * channel should already be registered with a Selector before + * construction of this object. + * @param bufferQueue + * The Queue that we should use to buffer data that hasn't been sent + * to the client yet. + * @param listener + * The {@link WebSocketListener} to notify of events when they occur. + * @param wstype + * The type of WebSocket, client or server. + */ + public WebSocket(SocketChannel socketChannel, + BlockingQueue bufferQueue, WebSocketListener listener, + ClientServerType wstype) { + this.socketChannel = socketChannel; + this.bufferQueue = bufferQueue; + this.handshakeComplete = false; + this.remoteHandshakeBuffer = this.currentFrame = null; + this.buffer = ByteBuffer.allocate(1); + this.wsl = listener; + this.wstype = wstype; + } + + /** + * Should be called when a Selector has a key that is writable for this + * WebSocket's SocketChannel connection. + * + * @throws IOException + * When socket related I/O errors occur. + * @throws NoSuchAlgorithmException + */ + void handleRead() throws IOException, NoSuchAlgorithmException { + this.buffer.rewind(); + + int bytesRead = -1; + try { + bytesRead = this.socketChannel.read(this.buffer); + } catch (Exception ex) { + } + + if (bytesRead == -1) { + close(); + } else if (bytesRead > 0) { + this.buffer.rewind(); + + if (!this.handshakeComplete) { + recieveHandshake(); + } else { + recieveFrame(); + } + } + } + + // PUBLIC INSTANCE METHODS ///////////////////////////////////////////////// + /** + * Closes the underlying SocketChannel, and calls the listener's onClose event + * handler. + * + * @throws IOException + * When socket related I/O errors occur. + */ + public void close() throws IOException { + this.socketChannel.close(); + this.wsl.onClose(this); + } + + /** + * @return True if all of the text was sent to the client by this thread. + * False if some of the text had to be buffered to be sent later. + */ + public boolean send(String text) throws IOException { + if (!this.handshakeComplete) + throw new NotYetConnectedException(); + if (text == null) + throw new NullPointerException("Cannot send 'null' data to a WebSocket."); + + // Get 'text' into a WebSocket "frame" of bytes + byte[] textBytes = text.getBytes(UTF8_CHARSET); + ByteBuffer b = ByteBuffer.allocate(textBytes.length + 2); + b.put(START_OF_FRAME); + b.put(textBytes); + b.put(END_OF_FRAME); + b.rewind(); + + // See if we have any backlog that needs to be sent first + if (handleWrite()) { + // Write the ByteBuffer to the socket + this.socketChannel.write(b); } - // PUBLIC INSTANCE METHODS ///////////////////////////////////////////////// - /** - * Should be called when a Selector has a key that is writable for this - * WebSocket's SocketChannel connection. - * @throws IOException When socket related I/O errors occur. - */ - public void handleRead() throws IOException { - this.buffer.rewind(); - int bytesRead = -1; - try { - bytesRead = this.socketChannel.read(this.buffer); - } catch(Exception ex) {} - - if (bytesRead == -1) - close(); - - if (bytesRead > 0) { - this.buffer.rewind(); - - if (!this.handshakeComplete) { - recieveHandshake(); - } else { - recieveFrame(); - } + // If we didn't get it all sent, add it to the buffer of buffers + if (b.remaining() > 0) { + if (!this.bufferQueue.offer(b)) { + throw new IOException("Buffers are full, message could not be sent to" + + this.socketChannel.socket().getRemoteSocketAddress()); + } + return false; + } + + return true; + } + + boolean hasBufferedData() { + return !this.bufferQueue.isEmpty(); + } + + /** + * @return True if all data has been sent to the client, false if there is + * still some buffered. + */ + boolean handleWrite() throws IOException { + synchronized (this.bufferQueueMutex) { + ByteBuffer buffer = this.bufferQueue.peek(); + while (buffer != null) { + this.socketChannel.write(buffer); + if (buffer.remaining() > 0) { + return false; // Didn't finish this buffer. There's more to send. + } else { + this.bufferQueue.poll(); // Buffer finished. Remove it. + buffer = this.bufferQueue.peek(); } + } + return true; + } + } + + public SocketChannel socketChannel() { + return this.socketChannel; + } + + // PRIVATE INSTANCE METHODS //////////////////////////////////////////////// + private void recieveFrame() { + byte newestByte = this.buffer.get(); + + if (newestByte == START_OF_FRAME) { // Beginning of Frame + this.currentFrame = null; + + } else if (newestByte == END_OF_FRAME) { // End of Frame + String textFrame = null; + // currentFrame will be null if END_OF_FRAME was send directly after + // START_OF_FRAME, thus we will send 'null' as the sent message. + if (this.currentFrame != null) + textFrame = new String(this.currentFrame.array(), UTF8_CHARSET); + this.wsl.onMessage(this, textFrame); + + } else { // Regular frame data, add to current frame buffer + ByteBuffer frame = ByteBuffer + .allocate((this.currentFrame != null ? this.currentFrame.capacity() + : 0) + + this.buffer.capacity()); + if (this.currentFrame != null) { + this.currentFrame.rewind(); + frame.put(this.currentFrame); + } + frame.put(newestByte); + this.currentFrame = frame; } + } - /** - * Closes the underlying SocketChannel, and calls the listener's onClose - * event handler. - * @throws IOException When socket related I/O errors occur. - */ - public void close() throws IOException { - this.socketChannel.close(); - this.wsl.onClose(this); + private void recieveHandshake() throws IOException, NoSuchAlgorithmException { + ByteBuffer ch = ByteBuffer.allocate((this.remoteHandshakeBuffer != null ? this.remoteHandshakeBuffer.capacity() : 0) + + this.buffer.capacity()); + if (this.remoteHandshakeBuffer != null) { + this.remoteHandshakeBuffer.rewind(); + ch.put(this.remoteHandshakeBuffer); } + ch.put(this.buffer); + this.remoteHandshakeBuffer = ch; - public void send(String text) throws IOException { - if (!this.handshakeComplete) throw new NotYetConnectedException(); - if (text == null) throw new NullPointerException("Cannot send 'null' data to a WebSocket."); + WebSocketHandshake handshake; - // Get 'text' into a WebSocket "frame" of bytes - byte[] textBytes = text.getBytes(UTF8_CHARSET); - ByteBuffer b = ByteBuffer.allocate(textBytes.length + 2); - b.put(START_OF_FRAME); - b.put(textBytes); - b.put(END_OF_FRAME); + byte[] h = this.remoteHandshakeBuffer.array(); - // Write the ByteBuffer to the socket - b.rewind(); - this.socketChannel.write(b); + // Check to see if this is a flash policy request + if (h.length == 23 && h[h.length - 1] == 0) { + handshake = new WebSocketHandshake(h); + completeHandshake(handshake); + return; } - public SocketChannel socketChannel() { - return this.socketChannel; + Draft draft; + + String hsString = new String(this.remoteHandshakeBuffer.array(), + UTF8_CHARSET); + if (hsString.toLowerCase().contains("\r\nsec-")) { + draft = Draft.DRAFT76; + } else { + draft = Draft.DRAFT75; } - // PRIVATE INSTANCE METHODS //////////////////////////////////////////////// - private void recieveFrame() { - byte newestByte = this.buffer.get(); - - if (newestByte == START_OF_FRAME) { // Beginning of Frame - this.currentFrame = null; - - } else if (newestByte == END_OF_FRAME) { // End of Frame - String textFrame = null; - // currentFrame will be null if END_OF_FRAME was send directly after - // START_OF_FRAME, thus we will send 'null' as the sent message. - if (this.currentFrame != null) - textFrame = new String(this.currentFrame.array(), UTF8_CHARSET); - this.wsl.onMessage(this, textFrame); - - } else { // Regular frame data, add to current frame buffer - ByteBuffer frame = ByteBuffer.allocate((this.currentFrame != null ? this.currentFrame.capacity() : 0) + this.buffer.capacity()); - if (this.currentFrame != null) { - this.currentFrame.rewind(); - frame.put(this.currentFrame); - } - frame.put(newestByte); - this.currentFrame = frame; - } + if (draft == Draft.DRAFT75 + && h.length >= 4 + && h[h.length - 4] == CR + && h[h.length - 3] == LF + && h[h.length - 2] == CR + && h[h.length - 1] == LF) { + + ClientServerType type = (wstype == ClientServerType.CLIENT) ? ClientServerType.SERVER : ClientServerType.CLIENT; + handshake = new WebSocketHandshake(h, type, draft); + completeHandshake(handshake); + return; + } - private void recieveHandshake() throws IOException { - ByteBuffer ch = ByteBuffer.allocate((this.remoteHandshakeBuffer != null ? this.remoteHandshakeBuffer.capacity() : 0) + this.buffer.capacity()); - if (this.remoteHandshakeBuffer != null) { - this.remoteHandshakeBuffer.rewind(); - ch.put(this.remoteHandshakeBuffer); - } - ch.put(this.buffer); - this.remoteHandshakeBuffer = ch; - - WebSocketHandshake handshake; - - byte[] h = this.remoteHandshakeBuffer.array(); - - // Check to see if this is a flash policy request - if (h.length==23 && h[h.length-1] == 0) { - handshake = new WebSocketHandshake(h); - completeHandshake(handshake); - return; - } + if (draft == Draft.DRAFT76 + && wstype == ClientServerType.SERVER + && h.length >= 12 + && h[h.length - 12] == CR + && h[h.length - 11] == LF + && h[h.length - 10] == CR + && h[h.length - 9] == LF) { + + handshake = new WebSocketHandshake(h, ClientServerType.CLIENT, draft); + completeHandshake(handshake); + return; - Draft draft; - - String hsString = new String(this.remoteHandshakeBuffer.array(), UTF8_CHARSET); - if (hsString.toLowerCase().contains("\r\nsec-")){ - draft = Draft.DRAFT76; - } else { - draft = Draft.DRAFT75; - } - - if (draft == Draft.DRAFT75 - &&h.length>=4 - && h[h.length-4] == CR - && h[h.length-3] == LF - && h[h.length-2] == CR - && h[h.length-1] == LF) { - - ClientServerType type = (wstype == ClientServerType.CLIENT) ? ClientServerType.SERVER : ClientServerType.CLIENT; - handshake = new WebSocketHandshake(h, type, draft); - completeHandshake(handshake); - return; - - } - - if (draft == Draft.DRAFT76 - && wstype == ClientServerType.SERVER - && h.length>=12 - && h[h.length-12] == CR - && h[h.length-11] == LF - && h[h.length-10] == CR - && h[h.length-9] == LF) { - - handshake = new WebSocketHandshake(h, ClientServerType.CLIENT, draft); - completeHandshake(handshake); - return; - - } - - if (draft == Draft.DRAFT76 - && wstype == ClientServerType.CLIENT - && h.length>=20 - && h[h.length-20] == CR - && h[h.length-19] == LF - && h[h.length-18] == CR - && h[h.length-17] == LF) { - - handshake = new WebSocketHandshake(h, ClientServerType.SERVER, draft); - completeHandshake(handshake); - return; - } } - private void completeHandshake(WebSocketHandshake handshake) throws IOException { - this.handshakeComplete = true; - if (this.wsl.onHandshakeRecieved(this, handshake)) { - this.wsl.onOpen(this); - } else { - close(); - } + if (draft == Draft.DRAFT76 + && wstype == ClientServerType.CLIENT + && h.length >= 20 + && h[h.length - 20] == CR + && h[h.length - 19] == LF + && h[h.length - 18] == CR + && h[h.length - 17] == LF) { + + handshake = new WebSocketHandshake(h, ClientServerType.SERVER, draft); + completeHandshake(handshake); + return; + } + } + + private void completeHandshake(WebSocketHandshake handshake) + throws IOException, NoSuchAlgorithmException { + this.handshakeComplete = true; + if (this.wsl.onHandshakeRecieved(this, handshake)) { + this.wsl.onOpen(this); + } else { + close(); } + } } diff --git a/src/WebSocketClient.java b/src/WebSocketClient.java index 56f8c122f..d21911e6c 100644 --- a/src/WebSocketClient.java +++ b/src/WebSocketClient.java @@ -13,457 +13,487 @@ import java.util.Iterator; import java.util.Random; import java.util.Set; - +import java.util.concurrent.LinkedBlockingQueue; /** * The WebSocketClient is an abstract class that expects a valid * "ws://" URI to connect to. When connected, an instance receives important * events related to the life of the connection. A subclass must implement - * onOpen, onClose, and onMessage to be - * useful. An instance can send messages to it's connected server via the + * onOpen, onClose, and onMessage to be useful. + * An instance can send messages to it's connected server via the * send method. + * * @author Nathan Rajlich */ public abstract class WebSocketClient implements Runnable, WebSocketListener { - // INSTANCE PROPERTIES ///////////////////////////////////////////////////// - /** - * The challenge for Draft 76 handshake - */ - private byte[] challenge = null; - /** - * The expected challenge respone for Draft 76 handshake - */ - private byte[] expected = null; - /** - * The version of the WebSocket Internet-Draft this client supports. - * Draft 76 by default. - */ - private Draft draft = Draft.DRAFT76; - /** - * The subprotocol this client object supports - */ - private String subprotocol = null; - /** - * The URI this client is supposed to connect to. - */ - private URI uri; - /** - * The WebSocket instance this client object wraps. - */ - private WebSocket conn; - - // CONSTRUCTOR ///////////////////////////////////////////////////////////// - /** - * Nullary constructor. You must call setURI before calling - * connect, otherwise an exception will be thrown. - */ - public WebSocketClient() {} - - /** - * Constructs a WebSocketClient instance and sets it to the connect to the - * specified URI. The client does not attempt to connect automatically. You - * must call connect first to initiate the socket connection. - * @param serverUri - */ - public WebSocketClient(URI serverUri) { - setURI(serverUri); - } - - /** - * Constructs a WebSocketClient instance and sets it to the connect to the - * specified URI using the specified subprotocol. The client does not - * attempt to connect automatically. You must call connect first - * to initiate the socket connection. - * @param serverUri - * @param subprotocol - */ - public WebSocketClient(URI serverUri, String subprotocol) { - setURI(serverUri); - setSubProtocol(subprotocol); - } - - /** - * Constructs a WebSocketClient instance and sets it to the connect to the - * specified URI using the specified subprotocol and draft. The client does not - * attempt to connect automatically. You must call connect first - * to initiate the socket connection. - * @param serverUri - * @param subprotocol - * @param draft DRAFT75 or DRAFT76 - */ - public WebSocketClient(URI serverUri, String subprotocol, String draft) { - setURI(serverUri); - setSubProtocol(subprotocol); - setDraft(Draft.valueOf(draft.toUpperCase())); - } - - // PUBLIC INSTANCE METHODS ///////////////////////////////////////////////// - /** - * Sets this WebSocketClient draft. - * @param draft - */ - public void setDraft(Draft draft) { - this.draft = draft; - } - - /** - * Gets the draft that this WebSocketClient supports. - * @return The draft for this WebSocketClient. - */ - public Draft getDraft() { - return draft; - } - - /** - * Sets this WebSocketClient subprotocol. - * @param subprotocol - */ - public void setSubProtocol(String subprotocol) { - this.subprotocol = subprotocol; - } - - /** - * Gets the subprotocol that this WebSocketClient supports. - * @return The subprotocol for this WebSocketClient. - */ - public String getSubProtocol() { - return subprotocol; - } - - /** - * Sets this WebSocketClient to connect to the specified URI. - * - * TODO: Throw an exception if this is called while the socket thread is - * running. - * - * @param uri - */ - public void setURI(URI uri) { - this.uri = uri; - } + // INSTANCE PROPERTIES ///////////////////////////////////////////////////// + /** + * The challenge for Draft 76 handshake + */ + private byte[] challenge = null; + /** + * The expected challenge respone for Draft 76 handshake + */ + private byte[] expected = null; + /** + * The version of the WebSocket Internet-Draft this client supports. Draft 76 + * by default. + */ + private Draft draft = Draft.DRAFT76; + /** + * The subprotocol this client object supports + */ + private String subprotocol = null; + /** + * The URI this client is supposed to connect to. + */ + private URI uri; + /** + * The WebSocket instance this client object wraps. + */ + private WebSocket conn; + + // CONSTRUCTOR ///////////////////////////////////////////////////////////// + /** + * Nullary constructor. You must call setURI before calling + * connect, otherwise an exception will be thrown. + */ + public WebSocketClient() { + } + + /** + * Constructs a WebSocketClient instance and sets it to the connect to the + * specified URI. The client does not attempt to connect automatically. You + * must call connect first to initiate the socket connection. + * + * @param serverUri + */ + public WebSocketClient(URI serverUri) { + setURI(serverUri); + } + + /** + * Constructs a WebSocketClient instance and sets it to the connect to the + * specified URI using the specified subprotocol. The client does not attempt + * to connect automatically. You must call connect first to + * initiate the socket connection. + * + * @param serverUri + * @param subprotocol + */ + public WebSocketClient(URI serverUri, String subprotocol) { + setURI(serverUri); + setSubProtocol(subprotocol); + } + + /** + * Constructs a WebSocketClient instance and sets it to the connect to the + * specified URI using the specified subprotocol and draft. The client does + * not attempt to connect automatically. You must call connect + * first to initiate the socket connection. + * + * @param serverUri + * @param subprotocol + * @param draft + * DRAFT75 or DRAFT76 + */ + public WebSocketClient(URI serverUri, String subprotocol, String draft) { + setURI(serverUri); + setSubProtocol(subprotocol); + setDraft(Draft.valueOf(draft.toUpperCase())); + } + + // PUBLIC INSTANCE METHODS ///////////////////////////////////////////////// + /** + * Sets this WebSocketClient draft. + * + * @param draft + */ + public void setDraft(Draft draft) { + this.draft = draft; + } + + /** + * Gets the draft that this WebSocketClient supports. + * + * @return The draft for this WebSocketClient. + */ + public Draft getDraft() { + return draft; + } + + /** + * Sets this WebSocketClient subprotocol. + * + * @param subprotocol + */ + public void setSubProtocol(String subprotocol) { + this.subprotocol = subprotocol; + } + + /** + * Gets the subprotocol that this WebSocketClient supports. + * + * @return The subprotocol for this WebSocketClient. + */ + public String getSubProtocol() { + return subprotocol; + } + + /** + * Sets this WebSocketClient to connect to the specified URI. + * + * TODO: Throw an exception if this is called while the socket thread is + * running. + * + * @param uri + */ + public void setURI(URI uri) { + this.uri = uri; + } + + /** + * Gets the URI that this WebSocketClient is connected to (or should attempt + * to connect to). + * + * @return The URI for this WebSocketClient. + */ + public URI getURI() { + return uri; + } + + /** + * Starts a background thread that attempts and maintains a WebSocket + * connection to the URI specified in the constructor or via + * setURI. setURI. + */ + public void connect() { + if (this.uri == null) + throw new NullPointerException( + "WebSocketClient must have a URI to connect to. See WebSocketClient#setURI"); + + (new Thread(this)).start(); + } + + /** + * Calls close on the underlying SocketChannel, which in turn + * closes the socket connection, and ends the client socket thread. + * + * @throws IOException + * When socket related I/O errors occur. + */ + public void close() throws IOException { + conn.close(); + } + + /** + * Sends text to the connected WebSocket server. + * + * @param text + * The String to send across the socket to the WebSocket server. + * @throws IOException + * When socket related I/O errors occur. + */ + public void send(String text) throws IOException { + conn.send(text); + } + + // Runnable IMPLEMENTATION ///////////////////////////////////////////////// + public void run() { + try { + int port = uri.getPort(); + if (port == -1) { + port = 80; + } + + // The WebSocket constructor expects a SocketChannel that is + // non-blocking, and has a Selector attached to it. + SocketChannel client = SocketChannel.open(); + client.configureBlocking(false); + client.connect(new InetSocketAddress(uri.getHost(), port)); + + Selector selector = Selector.open(); + + this.conn = new WebSocket(client, new LinkedBlockingQueue(), + this, ClientServerType.CLIENT); + client.register(selector, client.validOps()); + + // Continuous loop that is only supposed to end when close is called + while (selector.select(500) > 0) { + + Set keys = selector.selectedKeys(); + Iterator i = keys.iterator(); + + while (i.hasNext()) { + SelectionKey key = i.next(); + i.remove(); + + // When 'conn' has connected to the host + if (key.isConnectable()) { + // Ensure connection is finished + if (client.isConnectionPending()) + client.finishConnect(); + + sendClientHandshake(conn); + + } + + // When 'conn' has recieved some data + if (key.isReadable()) { + conn.handleRead(); + } + } + } - /** - * Gets the URI that this WebSocketClient is connected to (or should attempt - * to connect to). - * @return The URI for this WebSocketClient. - */ - public URI getURI() { - return uri; + } catch (IOException ex) { + ex.printStackTrace(); + } catch (NoSuchAlgorithmException ex) { + ex.printStackTrace(); } + } + + // WebSocketListener IMPLEMENTATION //////////////////////////////////////// + /** + * Parses the server's handshake to verify that it's a valid WebSocket + * handshake. + * + * @param conn + * The {@link WebSocket} instance who's handshake has been recieved. + * In the case of WebSocketClient, this.conn == conn. + * @param handshake + * The entire UTF-8 decoded handshake from the connection. + * @return true if handshake is a valid WebSocket server + * handshake, false otherwise. + * @throws IOException + * When socket related I/O errors occur. + * @throws NoSuchAlgorithmException + */ + public boolean onHandshakeRecieved(WebSocket conn, WebSocketHandshake handshake) throws IOException, NoSuchAlgorithmException { + + if (handshake.getDraft() == Draft.DRAFT75) { + // TODO: Do some parsing of the returned handshake, and close connection + // (return false) if we recieved anything unexpected. + return true; + } else if (handshake.getDraft() == Draft.DRAFT76) { + + if (!handshake.getProperty("status-code").equals("101")) { + return false; + } - /** - * Starts a background thread that attempts and maintains a WebSocket - * connection to the URI specified in the constructor or via setURI. - * setURI. - */ - public void connect() { - if (this.uri == null) throw new NullPointerException("WebSocketClient must have a URI to connect to. See WebSocketClient#setURI"); + if (!handshake.containsKey("upgrade") + || !handshake.getProperty("upgrade").equalsIgnoreCase("websocket")) { + return false; + } - (new Thread(this)).start(); - } + if (!handshake.containsKey("connection") + || !handshake.getProperty("connection").equalsIgnoreCase("upgrade")) { + return false; + } - /** - * Calls close on the underlying SocketChannel, which in turn - * closes the socket connection, and ends the client socket thread. - * @throws IOException When socket related I/O errors occur. - */ - public void close() throws IOException { - conn.close(); - } + if (!handshake.containsKey("sec-websocket-origin") + || !handshake.getProperty("sec-websocket-origin").equalsIgnoreCase( + "null")) { + return false; + } - /** - * Sends text to the connected WebSocket server. - * @param text The String to send across the socket to the WebSocket server. - * @throws IOException When socket related I/O errors occur. - */ - public void send(String text) throws IOException { - conn.send(text); - } + int port = (uri.getPort() == -1) ? 80 : uri.getPort(); + String location = "ws://"; + location += uri.getHost() + (port != 80 ? ":" + port : ""); + location += "/" + uri.getPath(); - // Runnable IMPLEMENTATION ///////////////////////////////////////////////// - public void run() { - try { - int port = uri.getPort(); - if (port == -1) { - port = 80; - } - - // The WebSocket constructor expects a SocketChannel that is - // non-blocking, and has a Selector attached to it. - SocketChannel client = SocketChannel.open(); - client.configureBlocking(false); - client.connect(new InetSocketAddress(uri.getHost(), port)); - - Selector selector = Selector.open(); - - this.conn = new WebSocket(client, this, ClientServerType.CLIENT); - client.register(selector, client.validOps()); - - // Continuous loop that is only supposed to end when close is called - while (selector.select(500) > 0) { - - Set keys = selector.selectedKeys(); - Iterator i = keys.iterator(); - - while (i.hasNext()) { - SelectionKey key = i.next(); - i.remove(); - - // When 'conn' has connected to the host - if (key.isConnectable()) { - // Ensure connection is finished - if (client.isConnectionPending()) - client.finishConnect(); - - sendClientHandshake(conn); - - } - - // When 'conn' has recieved some data - if (key.isReadable()) { - conn.handleRead(); - } - } - } - - } catch (IOException ex) { - ex.printStackTrace(); + if (!handshake.containsKey("sec-websocket-location") + || !handshake.getProperty("sec-websocket-location").equalsIgnoreCase( + location)) { + return false; + } + + if (subprotocol != null) { + // TODO: support lists of protocols + if (!handshake.containsKey("sec-websocket-protocol") + || !handshake.getProperty("sec-websocket-protocol").equals( + subprotocol)) { + return false; } - } + } + if (expected == null) { + MessageDigest md = MessageDigest.getInstance("MD5"); + expected = md.digest(challenge); + } - // WebSocketListener IMPLEMENTATION //////////////////////////////////////// - /** - * Parses the server's handshake to verify that it's a valid WebSocket - * handshake. - * @param conn The {@link WebSocket} instance who's handshake has been recieved. - * In the case of WebSocketClient, this.conn == conn. - * @param handshake The entire UTF-8 decoded handshake from the connection. - * @return true if handshake is a valid WebSocket server - * handshake, false otherwise. - * @throws IOException When socket related I/O errors occur. - */ - public boolean onHandshakeRecieved(WebSocket conn, WebSocketHandshake handshake) throws IOException { - - if (handshake.getDraft() == Draft.DRAFT75) { - // TODO: Do some parsing of the returned handshake, and close connection - // (return false) if we recieved anything unexpected. - return true; - } else if (handshake.getDraft() == Draft.DRAFT76) { - - if (!handshake.getProperty("status-code").equals("101")){ - return false; - } - - if (!handshake.containsKey("upgrade") || !handshake.getProperty("upgrade").equalsIgnoreCase("websocket")) { - return false; - } - - if (!handshake.containsKey("connection") || !handshake.getProperty("connection").equalsIgnoreCase("upgrade")) { - return false; - } - - if (!handshake.containsKey("sec-websocket-origin") || !handshake.getProperty("sec-websocket-origin").equalsIgnoreCase("null")) { - return false; - } - - int port = (uri.getPort() == -1) ? 80 : uri.getPort(); - String location = "ws://"; - location += uri.getHost() + (port != 80 ? ":" + port : ""); - location += "/" + uri.getPath(); - - if (!handshake.containsKey("sec-websocket-location") || !handshake.getProperty("sec-websocket-location").equalsIgnoreCase(location)) { - return false; - } - - if (subprotocol != null){ - // TODO: support lists of protocols - if (!handshake.containsKey("sec-websocket-protocol") || !handshake.getProperty("sec-websocket-protocol").equals(subprotocol)) { - return false; - } - } - - - if (expected == null) { - try { - MessageDigest md = MessageDigest.getInstance("MD5"); - expected = md.digest(challenge); - } catch (NoSuchAlgorithmException e) { - return false; - } - } - - byte[] reply = handshake.getAsByteArray("response"); - - if (!Arrays.equals(expected, reply)) { - return false; - } - - return true; - } - - // If we get here, then something must be wrong + byte[] reply = handshake.getAsByteArray("response"); + + if (!Arrays.equals(expected, reply)) { return false; - } - - /** - * Builds the handshake for this client. - * @throws IOException - */ - private void sendClientHandshake(WebSocket conn) throws IOException { - - int port = (uri.getPort() == -1) ? 80 : uri.getPort(); - String requestURI = "/" + uri.getPath(); - String host = uri.getHost() + (port != 80 ? ":" + port : ""); - String origin = "null"; - - WebSocketHandshake clientHandshake = new WebSocketHandshake(); - clientHandshake.setType(ClientServerType.CLIENT); - clientHandshake.setDraft(getDraft()); - - if (getDraft() == Draft.DRAFT75) { - clientHandshake.put("Request-Uri", requestURI); - clientHandshake.put("Host", host); - clientHandshake.put("Origin", origin); - } else { - clientHandshake.put("request-uri", requestURI); - clientHandshake.put("host", host); - clientHandshake.put("origin", origin); - } - - - if (subprotocol != null) { - if (getDraft() == Draft.DRAFT75) { - clientHandshake.put("Websocket-Protocol", subprotocol); - } else { - clientHandshake.put("sec-websocket-protocol", subprotocol); - } - } - - if (getDraft() == Draft.DRAFT76) { - - Random rand = new Random(); - - int spaces1 = rand.nextInt(11); - int spaces2 = rand.nextInt(11); - - spaces1+=2; - spaces2+=2; - - int max1 = Integer.MAX_VALUE / spaces1; - int max2 = Integer.MAX_VALUE / spaces2; - - int number1 = rand.nextInt(max1+1); - int number2 = rand.nextInt(max2+1); - - Integer product1 = number1 * spaces1; - Integer product2 = number2 * spaces2; - - String key1 = product1.toString(); - String key2 = product2.toString(); - - key1 = addNoise(key1); - key2 = addNoise(key2); - - key1 = addSpaces(key1,spaces1); - key2 = addSpaces(key2,spaces2); - - clientHandshake.put("sec-websocket-key1", key1); - clientHandshake.put("sec-websocket-key2", key2); - - byte[] key3 = new byte[8]; - rand.nextBytes(key3); - - clientHandshake.put("key3", key3); - - challenge = new byte[] { - (byte)(number1 >> 24), - (byte)(number1 >> 16), - (byte)(number1 >> 8), - (byte)(number1 >> 0), - (byte)(number2 >> 24), - (byte)(number2 >> 16), - (byte)(number2 >> 8), - (byte)(number2 >> 0), - (byte) key3[0], - (byte) key3[1], - (byte) key3[2], - (byte) key3[3], - (byte) key3[4], - (byte) key3[5], - (byte) key3[6], - (byte) key3[7] - }; - - } - - conn.socketChannel().write(ByteBuffer.wrap(clientHandshake.getHandshake())); - + } + + return true; } - /** - * Calls subclass' implementation of onMessage. - * @param conn - * @param message - */ - public void onMessage(WebSocket conn, String message) { - onMessage(message); + // If we get here, then something must be wrong + return false; + } + + /** + * Builds the handshake for this client. + * + * @throws IOException + */ + private void sendClientHandshake(WebSocket conn) throws IOException { + + int port = (uri.getPort() == -1) ? 80 : uri.getPort(); + String requestURI = "/" + uri.getPath(); + String host = uri.getHost() + (port != 80 ? ":" + port : ""); + String origin = "null"; + + WebSocketHandshake clientHandshake = new WebSocketHandshake(); + clientHandshake.setType(ClientServerType.CLIENT); + clientHandshake.setDraft(getDraft()); + + if (getDraft() == Draft.DRAFT75) { + clientHandshake.put("Request-Uri", requestURI); + clientHandshake.put("Host", host); + clientHandshake.put("Origin", origin); + } else { + clientHandshake.put("request-uri", requestURI); + clientHandshake.put("host", host); + clientHandshake.put("origin", origin); } - /** - * Calls subclass' implementation of onOpen. - * @param conn - */ - public void onOpen(WebSocket conn) { - onOpen(); + if (subprotocol != null) { + if (getDraft() == Draft.DRAFT75) { + clientHandshake.put("Websocket-Protocol", subprotocol); + } else { + clientHandshake.put("sec-websocket-protocol", subprotocol); + } } - /** - * Calls subclass' implementation of onClose. - * @param conn - */ - public void onClose(WebSocket conn) { - onClose(); + if (getDraft() == Draft.DRAFT76) { + + Random rand = new Random(); + + int spaces1 = rand.nextInt(11); + int spaces2 = rand.nextInt(11); + + spaces1 += 2; + spaces2 += 2; + + int max1 = Integer.MAX_VALUE / spaces1; + int max2 = Integer.MAX_VALUE / spaces2; + + int number1 = rand.nextInt(max1 + 1); + int number2 = rand.nextInt(max2 + 1); + + Integer product1 = number1 * spaces1; + Integer product2 = number2 * spaces2; + + String key1 = product1.toString(); + String key2 = product2.toString(); + + key1 = addNoise(key1); + key2 = addNoise(key2); + + key1 = addSpaces(key1, spaces1); + key2 = addSpaces(key2, spaces2); + + clientHandshake.put("sec-websocket-key1", key1); + clientHandshake.put("sec-websocket-key2", key2); + + byte[] key3 = new byte[8]; + rand.nextBytes(key3); + + clientHandshake.put("key3", key3); + + challenge = new byte[] { + (byte) (number1 >> 24), + (byte) (number1 >> 16), + (byte) (number1 >> 8), + (byte) (number1 >> 0), + (byte) (number2 >> 24), + (byte) (number2 >> 16), + (byte) (number2 >> 8), + (byte) (number2 >> 0), + (byte) key3[0], + (byte) key3[1], + (byte) key3[2], + (byte) key3[3], + (byte) key3[4], + (byte) key3[5], + (byte) key3[6], + (byte) key3[7] }; + } - private String addNoise (String key) { - - Random rand = new Random(); - - for (int i = 0; i < (rand.nextInt(12) + 1); i++) { - // get a random non-numeric character - int x = 0; - while (x < 33 || ( x >= 48 && x <= 57)) { - x = (rand.nextInt(93) + 33); - } - char r = (char) x; - // get a random position in key - int pos = rand.nextInt(key.length()+1); - key = key.substring(0,pos) + r + key.substring(pos); - } - - return key; - + conn.socketChannel().write(ByteBuffer.wrap(clientHandshake.getHandshake())); + + } + + /** + * Calls subclass' implementation of onMessage. + * + * @param conn + * @param message + */ + public void onMessage(WebSocket conn, String message) { + onMessage(message); + } + + /** + * Calls subclass' implementation of onOpen. + * + * @param conn + */ + public void onOpen(WebSocket conn) { + onOpen(); + } + + /** + * Calls subclass' implementation of onClose. + * + * @param conn + */ + public void onClose(WebSocket conn) { + onClose(); + } + + private String addNoise(String key) { + + Random rand = new Random(); + + for (int i = 0; i < (rand.nextInt(12) + 1); i++) { + // get a random non-numeric character + int x = 0; + while (x < 33 || (x >= 48 && x <= 57)) { + x = (rand.nextInt(93) + 33); + } + char r = (char) x; + // get a random position in key + int pos = rand.nextInt(key.length() + 1); + key = key.substring(0, pos) + r + key.substring(pos); } - - private String addSpaces (String key, int spaces) { - - Random rand = new Random(); - - for (int i = 0; i < spaces; i++) { - char space = (char) 32; - // get a random position in key that is not - int pos = rand.nextInt(key.length()-1) + 1; - key = key.substring(0,pos) + space + key.substring(pos); - } - - return key; - + + return key; + + } + + private String addSpaces(String key, int spaces) { + + Random rand = new Random(); + + for (int i = 0; i < spaces; i++) { + char space = (char) 32; + // get a random position in key that is not + int pos = rand.nextInt(key.length() - 1) + 1; + key = key.substring(0, pos) + space + key.substring(pos); } - - // ABTRACT METHODS ///////////////////////////////////////////////////////// - public abstract void onMessage(String message); - public abstract void onOpen(); - public abstract void onClose(); - + + return key; + + } + + // ABTRACT METHODS ///////////////////////////////////////////////////////// + public abstract void onMessage(String message); + public abstract void onOpen(); + public abstract void onClose(); + } diff --git a/src/WebSocketHandshake.java b/src/WebSocketHandshake.java index ecf220cb5..c8d45e7bc 100644 --- a/src/WebSocketHandshake.java +++ b/src/WebSocketHandshake.java @@ -5,304 +5,325 @@ import java.util.Hashtable; /** - * The WebSocketHandshake is a class used to create and read client and server handshakes. + * The WebSocketHandshake is a class used to create and read client and + * server handshakes. + * * @author Nathan Mishe */ -public class WebSocketHandshake extends Hashtable implements WebSocketProtocol { - - // INSTANCE PROPERTIES ///////////////////////////////////////////////////// - private static final long serialVersionUID = -280930290679993232L; - /** - * The raw handshake. - */ - private byte[] handshake; - /** - * The WebSocket Internet-Draft version. - */ - private Draft handshakeDraft; - /** - * The type of the handshake. The refers to the source of the handshake, either - * client or server. - */ - private ClientServerType handshakeType; - - // CONSTRUCTOR ///////////////////////////////////////////////////////////// - public WebSocketHandshake() { - super(); - } - - public WebSocketHandshake(byte[] handshake) { - super(); - setHandshake(handshake); - } - - public WebSocketHandshake(byte[] handshake, ClientServerType type, Draft draft) { - super(); - setDraft(draft); - setType(type); - setHandshake(handshake); - } - - // PUBLIC INSTANCE METHODS ///////////////////////////////////////////////// - public void setDraft(Draft handshakeDraft) { - this.handshakeDraft = handshakeDraft; - } - - public Draft getDraft() { - return handshakeDraft; +public class WebSocketHandshake extends Hashtable implements + WebSocketProtocol { + + // INSTANCE PROPERTIES ///////////////////////////////////////////////////// + private static final long serialVersionUID = -280930290679993232L; + /** + * The raw handshake. + */ + private byte[] handshake; + /** + * The WebSocket Internet-Draft version. + */ + private Draft handshakeDraft; + /** + * The type of the handshake. The refers to the source of the handshake, + * either client or server. + */ + private ClientServerType handshakeType; + + // CONSTRUCTOR ///////////////////////////////////////////////////////////// + public WebSocketHandshake() { + super(); + } + + public WebSocketHandshake(byte[] handshake) { + super(); + setHandshake(handshake); + } + + public WebSocketHandshake(byte[] handshake, ClientServerType type, Draft draft) { + super(); + setDraft(draft); + setType(type); + setHandshake(handshake); + } + + // PUBLIC INSTANCE METHODS ///////////////////////////////////////////////// + public void setDraft(Draft handshakeDraft) { + this.handshakeDraft = handshakeDraft; + } + + public Draft getDraft() { + return handshakeDraft; + } + + public void setHandshake(byte[] handshake) { + this.handshake = handshake; + if (handshakeDraft != null && handshakeType != null) { + parseHandshake(); } + } - public void setHandshake(byte[] handshake) { - this.handshake = handshake; - if (handshakeDraft != null && handshakeType != null) { - parseHandshake(); - } + public byte[] getHandshake() { + if (handshake == null && handshakeDraft != null && handshakeType != null) { + buildHandshake(); } + return handshake; + } - public byte[] getHandshake() { - if (handshake == null && handshakeDraft != null && handshakeType != null) { - buildHandshake(); - } - return handshake; + public void setType(ClientServerType handshakeType) { + this.handshakeType = handshakeType; + } + + public ClientServerType getType() { + return handshakeType; + } + + public String toString() { + return new String(getHandshake(), UTF8_CHARSET); + } + + public String getProperty(String fieldName) { + if (!containsKey(fieldName)) { + return null; } + return (String) get(fieldName); + } - public void setType(ClientServerType handshakeType) { - this.handshakeType = handshakeType; + public byte[] getAsByteArray(String fieldName) { + if (!containsKey(fieldName)) { + return null; } + return (byte[]) get(fieldName); + } + + /** + * Parses the parts of the handshake into Hashtable keys. Can only be called + * if the handshake draft and type are set. Called automatically when + * handshake is set, if the draft and type are known and + * set for the handshake. + */ + public void parseHandshake() { + + if (this.handshakeDraft == null) + throw new NullPointerException( + "Handshake draft type must be set before parsing."); + if (this.handshakeType == null) + throw new NullPointerException( + "Handshake type type must be set before parsing."); + + // parse the HTTP request-line or HTTP status-line + + String hs = this.toString(); + String[] requestLines = hs.split("\r\n"); + String line = requestLines[0].trim(); - public ClientServerType getType() { - return handshakeType; + if (this.handshakeType == ClientServerType.SERVER) { + int sp1 = line.indexOf(" "); + int sp2 = line.indexOf(" ", sp1 + 1); + String httpVersion = line.substring(0, sp1); + String statusCode = line.substring(sp1 + 1, sp2); + String reasonPhrase = line.substring(sp2 + 1, line.length()); + String httpVersionKey = "HTTP-Version"; + String statusCodeKey = "Status-Code"; + String reasonPhraseKey = "Reason-Phrase"; + + if (this.handshakeDraft == Draft.DRAFT76) { + httpVersionKey = httpVersionKey.toLowerCase(); + statusCodeKey = statusCodeKey.toLowerCase(); + reasonPhraseKey = reasonPhraseKey.toLowerCase(); + } + + put(httpVersionKey, httpVersion); + put(statusCodeKey, statusCode); + put(reasonPhraseKey, reasonPhrase); } - public String toString() { - return new String(getHandshake(),UTF8_CHARSET); + if (this.handshakeType == ClientServerType.CLIENT) { + int sp1 = line.indexOf(" "); + int sp2 = line.indexOf(" ", sp1 + 1); + String method = line.substring(0, sp1); + String requestURI = line.substring(sp1 + 1, sp2); + String httpVersion = line.substring(sp2 + 1, line.length()); + String methodKey = "Method"; + String requestURIKey = "Request-URI"; + String httpVersionKey = "HTTP-Version"; + + if (this.handshakeDraft == Draft.DRAFT76) { + methodKey = methodKey.toLowerCase(); + requestURIKey = requestURIKey.toLowerCase(); + httpVersionKey = httpVersionKey.toLowerCase(); + } + + put(methodKey, method); + put(requestURIKey, requestURI); + put(httpVersionKey, httpVersion); } - public String getProperty(String fieldName) { - if (!containsKey(fieldName)) { - return null; - } - return (String) get(fieldName); + // parse fields + + for (int i = 1; i < requestLines.length; i++) { + line = requestLines[i]; + if (line.length() == 0) { + break; + } + int firstColon = line.indexOf(":"); + String keyName = line.substring(0, firstColon).trim(); + String keyValue = line.substring(firstColon + 1).trim(); + if (this.handshakeDraft == Draft.DRAFT76) { + keyName = keyName.toLowerCase(); + } + put(keyName, keyValue); } - public byte[] getAsByteArray(String fieldName) { - if (!containsKey(fieldName)) { - return null; - } - return (byte[]) get(fieldName); + // get the key3 bytes + + if (this.handshakeDraft == Draft.DRAFT76) { + int l = getHandshake().length; + if (this.handshakeType == ClientServerType.CLIENT) { + // get last 8 bytes + byte[] key3 = { + getHandshake()[l - 8], + getHandshake()[l - 7], + getHandshake()[l - 6], + getHandshake()[l - 5], + getHandshake()[l - 4], + getHandshake()[l - 3], + getHandshake()[l - 2], + getHandshake()[l - 1] }; + put("key3", key3); + } + if (this.handshakeType == ClientServerType.SERVER) { + // get last 16 bytes + byte[] response = { + getHandshake()[l - 16], + getHandshake()[l - 15], + getHandshake()[l - 14], + getHandshake()[l - 13], + getHandshake()[l - 12], + getHandshake()[l - 11], + getHandshake()[l - 10], + getHandshake()[l - 9], + getHandshake()[l - 8], + getHandshake()[l - 7], + getHandshake()[l - 6], + getHandshake()[l - 5], + getHandshake()[l - 4], + getHandshake()[l - 3], + getHandshake()[l - 2], + getHandshake()[l - 1] }; + put("response", response); + } } + } - /** - * Parses the parts of the handshake into Hashtable keys. Can only be - * called if the handshake draft and type are set. Called automatically - * when handshake is set, if the draft and type are known and set for - * the handshake. - */ - public void parseHandshake() { - - if (this.handshakeDraft == null) throw new NullPointerException("Handshake draft type must be set before parsing."); - if (this.handshakeType == null) throw new NullPointerException("Handshake type type must be set before parsing."); - - // parse the HTTP request-line or HTTP status-line - - String hs = this.toString(); - String[] requestLines = hs.split("\r\n"); - String line = requestLines[0].trim(); - - if (this.handshakeType == ClientServerType.SERVER) { - int sp1 = line.indexOf(" "); - int sp2 = line.indexOf(" ",sp1+1); - String httpVersion = line.substring(0,sp1); - String statusCode = line.substring(sp1+1,sp2); - String reasonPhrase = line.substring(sp2+1,line.length()); - String httpVersionKey = "HTTP-Version"; - String statusCodeKey = "Status-Code"; - String reasonPhraseKey = "Reason-Phrase"; - - if (this.handshakeDraft == Draft.DRAFT76) { - httpVersionKey = httpVersionKey.toLowerCase(); - statusCodeKey = statusCodeKey.toLowerCase(); - reasonPhraseKey = reasonPhraseKey.toLowerCase(); - } - - put(httpVersionKey, httpVersion); - put(statusCodeKey, statusCode); - put(reasonPhraseKey, reasonPhrase); - } - - if (this.handshakeType == ClientServerType.CLIENT) { - int sp1 = line.indexOf(" "); - int sp2 = line.indexOf(" ",sp1+1); - String method = line.substring(0,sp1); - String requestURI = line.substring(sp1+1,sp2); - String httpVersion = line.substring(sp2+1,line.length()); - String methodKey = "Method"; - String requestURIKey = "Request-URI"; - String httpVersionKey = "HTTP-Version"; - - if (this.handshakeDraft == Draft.DRAFT76) { - methodKey = methodKey.toLowerCase(); - requestURIKey = requestURIKey.toLowerCase(); - httpVersionKey = httpVersionKey.toLowerCase(); - } - - put(methodKey, method); - put(requestURIKey, requestURI); - put(httpVersionKey, httpVersion); + /** + * Generates the handshake byte array based on the handshake draft + * and type as well as the keys set for this handshake. Called + * automatically when handshake is requested, if the draft and + * type are known and set for the handshake. + */ + public void buildHandshake() { + + if (this.handshakeDraft == null) + throw new NullPointerException( + "Handshake draft type must be set before building."); + if (this.handshakeType == null) + throw new NullPointerException( + "Handshake type type must be set before building."); + + if (this.handshakeType == ClientServerType.SERVER) { + + String responseHandshake = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" + + "Upgrade: WebSocket\r\n" + "Connection: Upgrade\r\n"; + + if (this.handshakeDraft == Draft.DRAFT75) { + responseHandshake += "WebSocket-Origin: " + getProperty("Origin") + + "\r\n" + "WebSocket-Location: ws://" + getProperty("Host") + + getProperty("Request-URI") + "\r\n"; + if (containsKey("WebSocket-Protocol")) { + responseHandshake += "WebSocket-Protocol: " + + getProperty("Websocket-Protocol") + "\r\n"; } - - // parse fields - - for (int i=1; idraft and type as well as the keys - * set for this handshake. Called automatically - * when handshake is requested, if the draft and type are known and set for - * the handshake. - */ - public void buildHandshake() { - - if (this.handshakeDraft == null) throw new NullPointerException("Handshake draft type must be set before building."); - if (this.handshakeType == null) throw new NullPointerException("Handshake type type must be set before building."); - - if (this.handshakeType == ClientServerType.SERVER) { - - String responseHandshake = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" + - "Upgrade: WebSocket\r\n" + - "Connection: Upgrade\r\n"; - - if (this.handshakeDraft == Draft.DRAFT75) { - responseHandshake += "WebSocket-Origin: " + getProperty("Origin") + "\r\n" + - "WebSocket-Location: ws://" + getProperty("Host") + getProperty("Request-URI") + "\r\n"; - if (containsKey("WebSocket-Protocol")) { - responseHandshake += "WebSocket-Protocol: " + getProperty("Websocket-Protocol") + "\r\n"; - } - } - - if (this.handshakeDraft == Draft.DRAFT76) { - responseHandshake += "Sec-WebSocket-Location: ws://" + getProperty("host") + getProperty("request-uri") + "\r\n" + - "Sec-WebSocket-Origin: " + getProperty("origin") + "\r\n"; - if (containsKey("sec-websocket-protocol")) { - responseHandshake += "Sec-WebSocket-Protocol: " + getProperty("sec-websocket-protocol") + "\r\n"; - } - - } - - responseHandshake += "\r\n"; - - byte[] responseHandshakeBytes = responseHandshake.getBytes(UTF8_CHARSET); - - try{ - ByteArrayOutputStream buff = new ByteArrayOutputStream(); - buff.write(responseHandshakeBytes); - if (this.handshakeDraft == Draft.DRAFT76) { - byte[] response = getAsByteArray("response"); - buff.write(response); - } - this.handshake = buff.toByteArray(); - return; - } catch(IOException e) { - System.out.print(e); - }; - + + if (this.handshakeType == ClientServerType.CLIENT) { + + String requestURI = (getDraft() == Draft.DRAFT75) ? getProperty("Request-URI") + : getProperty("request-uri"); + String host = (getDraft() == Draft.DRAFT75) ? getProperty("Host") + : getProperty("host"); + String origin = (getDraft() == Draft.DRAFT75) ? getProperty("Origin") + : getProperty("origin"); + + String requestHandshake = "GET " + requestURI + " HTTP/1.1\r\n" + + "Upgrade: WebSocket\r\n" + "Connection: Upgrade\r\n" + "Host: " + + host + "\r\n" + "Origin: " + origin + "\r\n"; + + if (getDraft() == Draft.DRAFT75 && containsKey("Websocket-Protocol")) { + requestHandshake += "WebSocket-Protocol: " + + getProperty("Websocket-Protocol") + "\r\n"; + } + + if (getDraft() == Draft.DRAFT76) { + if (containsKey("sec-webSocket-protocol")) { + requestHandshake += "Sec-WebSocket-Protocol: " + + getProperty("sec-websocket-protocol") + "\r\n"; } - - if (this.handshakeType == ClientServerType.CLIENT) { - - String requestURI = (getDraft() == Draft.DRAFT75) ? getProperty("Request-URI") : getProperty("request-uri"); - String host = (getDraft() == Draft.DRAFT75) ? getProperty("Host") : getProperty("host"); - String origin = (getDraft() == Draft.DRAFT75) ? getProperty("Origin") : getProperty("origin"); - - String requestHandshake = "GET " + requestURI + " HTTP/1.1\r\n" + - "Upgrade: WebSocket\r\n" + - "Connection: Upgrade\r\n" + - "Host: " + host + "\r\n" + - "Origin: " + origin + "\r\n"; - - if (getDraft() == Draft.DRAFT75 && containsKey("Websocket-Protocol")) { - requestHandshake += "WebSocket-Protocol: " + getProperty("Websocket-Protocol") + "\r\n"; - } - - if (getDraft() == Draft.DRAFT76 ) { - if(containsKey("sec-webSocket-protocol")) { - requestHandshake += "Sec-WebSocket-Protocol: " + getProperty("sec-websocket-protocol") + "\r\n"; - } - requestHandshake += "Sec-WebSocket-Key1: " + getProperty("sec-websocket-key1") + "\r\n"; - requestHandshake += "Sec-WebSocket-Key2: " + getProperty("sec-websocket-key2") + "\r\n"; - } - - requestHandshake += "\r\n"; - - byte[] requestHandshakeBytes = requestHandshake.getBytes(UTF8_CHARSET); - - try{ - ByteArrayOutputStream buff = new ByteArrayOutputStream(); - buff.write(requestHandshakeBytes); - if (this.handshakeDraft == Draft.DRAFT76) { - byte[] key3 = getAsByteArray("key3"); - buff.write(key3); - } - this.handshake = buff.toByteArray(); - return; - } catch(IOException e) { - System.out.print(e); - }; - + requestHandshake += "Sec-WebSocket-Key1: " + + getProperty("sec-websocket-key1") + "\r\n"; + requestHandshake += "Sec-WebSocket-Key2: " + + getProperty("sec-websocket-key2") + "\r\n"; + } + + requestHandshake += "\r\n"; + + byte[] requestHandshakeBytes = requestHandshake.getBytes(UTF8_CHARSET); + + try { + ByteArrayOutputStream buff = new ByteArrayOutputStream(); + buff.write(requestHandshakeBytes); + if (this.handshakeDraft == Draft.DRAFT76) { + byte[] key3 = getAsByteArray("key3"); + buff.write(key3); } - + this.handshake = buff.toByteArray(); + return; + } catch (IOException e) { + System.out.print(e); + } + ; + } + + } } diff --git a/src/WebSocketListener.java b/src/WebSocketListener.java index a64f61642..84656fb9d 100644 --- a/src/WebSocketListener.java +++ b/src/WebSocketListener.java @@ -1,42 +1,58 @@ - import java.io.IOException; +import java.security.NoSuchAlgorithmException; /** - * Implemented by WebSocketClient and WebSocketServer. - * The methods within are called by WebSocket. + * Implemented by WebSocketClient and WebSocketServer. The + * methods within are called by WebSocket. + * * @author Nathan Rajlich */ interface WebSocketListener extends WebSocketProtocol { - /** - * Called when the socket connection is first established, and the WebSocket - * handshake has been recieved. This method should parse the - * handshake, and return a boolean indicating whether or not the - * connection is a valid WebSocket connection. - * @param conn The WebSocket instance this event is occuring on. - * @param handshake The WebSocketHandshake. - * @return true if the handshake is valid, and onOpen - * should be immediately called afterwards. false if the - * handshake was invalid, and the connection should be terminated. - */ - public boolean onHandshakeRecieved(WebSocket conn, WebSocketHandshake handshake) throws IOException; - /** - * Called when an entire text frame has been recieved. Do whatever you want - * here... - * @param conn The WebSocket instance this event is occuring on. - * @param message The UTF-8 decoded message that was recieved. - */ - public void onMessage(WebSocket conn, String message); - /** - * Called after onHandshakeRecieved returns true. - * Indicates that a complete WebSocket connection has been established, - * and we are ready to send/recieve data. - * @param conn The WebSocket instance this event is occuring on. - */ - public void onOpen(WebSocket conn); - /** - * Called after WebSocket#close is explicity called, or when the - * other end of the WebSocket connection is closed. - * @param conn The WebSocket instance this event is occuring on. - */ - public void onClose(WebSocket conn); + /** + * Called when the socket connection is first established, and the WebSocket + * handshake has been recieved. This method should parse the + * handshake, and return a boolean indicating whether or not the + * connection is a valid WebSocket connection. + * + * @param conn + * The WebSocket instance this event is occuring on. + * @param handshake + * The WebSocketHandshake. + * @return true if the handshake is valid, and onOpen + * should be immediately called afterwards. false if the + * handshake was invalid, and the connection should be terminated. + */ + public boolean onHandshakeRecieved(WebSocket conn, + WebSocketHandshake handshake) throws IOException, + NoSuchAlgorithmException; + + /** + * Called when an entire text frame has been recieved. Do whatever you want + * here... + * + * @param conn + * The WebSocket instance this event is occuring on. + * @param message + * The UTF-8 decoded message that was recieved. + */ + public void onMessage(WebSocket conn, String message); + + /** + * Called after onHandshakeRecieved returns true. + * Indicates that a complete WebSocket connection has been established, and we + * are ready to send/recieve data. + * + * @param conn + * The WebSocket instance this event is occuring on. + */ + public void onOpen(WebSocket conn); + + /** + * Called after WebSocket#close is explicity called, or when the + * other end of the WebSocket connection is closed. + * + * @param conn + * The WebSocket instance this event is occuring on. + */ + public void onClose(WebSocket conn); } diff --git a/src/WebSocketProtocol.java b/src/WebSocketProtocol.java index e7f48e124..685d0cc54 100644 --- a/src/WebSocketProtocol.java +++ b/src/WebSocketProtocol.java @@ -1,36 +1,33 @@ - import java.nio.charset.Charset; /** * Constants used by WebSocket protocol. + * * @author Nathan Mische */ interface WebSocketProtocol { - - /** - * The version of the WebSocket Internet-Draft - */ - public enum Draft { - AUTO, - DRAFT75, - DRAFT76 - } - - /** - * The maximum value of a WebSocket key. - */ - public static final Long MAX_KEY_VALUE = Long.parseLong("4294967295"); - - /** - * The WebSocket protocol expects UTF-8 encoded bytes. - */ - public static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); - - /** - * The type of WebSocket - */ - public enum ClientServerType { - CLIENT, - SERVER - } + + /** + * The version of the WebSocket Internet-Draft + */ + public enum Draft { + AUTO, DRAFT75, DRAFT76 + } + + /** + * The maximum value of a WebSocket key. + */ + public static final Long MAX_KEY_VALUE = Long.parseLong("4294967295"); + + /** + * The WebSocket protocol expects UTF-8 encoded bytes. + */ + public static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); + + /** + * The type of WebSocket + */ + public enum ClientServerType { + CLIENT, SERVER + } } diff --git a/src/WebSocketServer.java b/src/WebSocketServer.java index 4c7b49d67..121639d33 100644 --- a/src/WebSocketServer.java +++ b/src/WebSocketServer.java @@ -1,4 +1,3 @@ - import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; @@ -9,553 +8,706 @@ import java.security.NoSuchAlgorithmException; import java.util.Iterator; import java.util.Set; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.CopyOnWriteArraySet; - +import java.util.concurrent.LinkedBlockingQueue; /** * WebSocketServer is an abstract class that only takes care of the * HTTP handshake portion of WebSockets. It's up to a subclass to add * functionality/purpose to the server. * - * May be configured to listen to listen on a specified - * port using a specified subprotocol. - * May also be configured to only allows connections from a specified origin. - * Can be configure to support a specific draft of the WebSocket protocol - * (DRAFT75 or DRAFT76) or both (AUTO). + * May be configured to listen to listen on a specified port using a + * specified subprotocol. May also be configured to only allows + * connections from a specified origin. Can be configure to support a + * specific draft of the WebSocket protocol (DRAFT75 or DRAFT76) or + * both (AUTO). + * * @author Nathan Rajlich */ public abstract class WebSocketServer implements Runnable, WebSocketListener { - // CONSTANTS /////////////////////////////////////////////////////////////// - /** - * If the nullary constructor is used, the DEFAULT_PORT will be the port - * the WebSocketServer is binded to. - */ - public static final int DEFAULT_PORT = 80; - /** - * If a constructor is used that doesn't define the draft, the DEFAULT_DRAFT - * will be the draft of the WebSocket protocl the WebSocketServer supports. - */ - public static final Draft DEFAULT_DRAFT = Draft.AUTO; - /** - * The value of handshake when a Flash client requests a policy - * file on this server. - */ - public static final String FLASH_POLICY_REQUEST = "\0"; - - // INSTANCE PROPERTIES ///////////////////////////////////////////////////// - /** - * Holds the list of active WebSocket connections. "Active" means WebSocket - * handshake is complete and socket can be written to, or read from. - */ - private final CopyOnWriteArraySet connections; - /** - * The version of the WebSocket Internet-Draft this client supports. - */ - private Draft draft; - /** - * The origin this WebSocket server will accept connections from. - */ - private String origin; - /** - * The port number that this WebSocket server should listen on. Default is - * 80 (HTTP). - */ - private int port; - /** - * The socket channel for this WebSocket server. - */ - private ServerSocketChannel server; - /** - * The subprotocol that this WebSocket server supports. Default is null. - */ - private String subprotocol; - - // CONSTRUCTOR ///////////////////////////////////////////////////////////// - /** - * Nullary constructor. Creates a WebSocketServer that will attempt to - * listen on port DEFAULT_PORT and support both draft 75 and 76 of the - * WebSocket protocol. - */ - public WebSocketServer() { - this(DEFAULT_PORT,null,null,DEFAULT_DRAFT); + // CONSTANTS /////////////////////////////////////////////////////////////// + /** + * If the nullary constructor is used, the DEFAULT_PORT will be the port the + * WebSocketServer is binded to. + */ + public static final int DEFAULT_PORT = 80; + /** + * If a constructor is used that doesn't define the draft, the DEFAULT_DRAFT + * will be the draft of the WebSocket protocl the WebSocketServer supports. + */ + public static final Draft DEFAULT_DRAFT = Draft.AUTO; + /** + * The value of handshake when a Flash client requests a policy + * file on this server. + */ + public static final String FLASH_POLICY_REQUEST = "\0"; + + // INSTANCE PROPERTIES ///////////////////////////////////////////////////// + /** + * Holds the list of active WebSocket connections. "Active" means WebSocket + * handshake is complete and socket can be written to, or read from. + */ + private final CopyOnWriteArraySet connections; + /** + * The version of the WebSocket Internet-Draft this client supports. + */ + private Draft draft; + /** + * The origin this WebSocket server will accept connections from. + */ + private String origin; + /** + * The port number that this WebSocket server should listen on. Default is 80 + * (HTTP). + */ + private int port; + /** + * The socket channel for this WebSocket server. + */ + private ServerSocketChannel server; + /** + * The subprotocol that this WebSocket server supports. Default is null. + */ + private String subprotocol; + + // CONSTRUCTOR ///////////////////////////////////////////////////////////// + /** + * Nullary constructor. Creates a WebSocketServer that will attempt to listen + * on port DEFAULT_PORT and support both draft 75 and 76 of the WebSocket + * protocol. + */ + public WebSocketServer() { + this(DEFAULT_PORT, null, null, DEFAULT_DRAFT); + } + + /** + * Creates a WebSocketServer that will attempt to listen on port + * port. + * + * @param port + * The port number this server should listen on. + */ + public WebSocketServer(int port) { + this(port, null, null, DEFAULT_DRAFT); + } + + /** + * Creates a WebSocketServer that will attempt to listen on port + * port. Only allows connections from origin. + * + * @param port + * The port number this server should listen on. + * @param origin + * The origin this server supports. + */ + public WebSocketServer(int port, String origin) { + this(port, origin, null, DEFAULT_DRAFT); + } + + /** + * Creates a WebSocketServer that will attempt to listen on port + * port using a specified subprotocol. Only allows + * connections from origin. + * + * @param port + * The port number this server should listen on. + * @param origin + * The origin this server supports. + * @param subprotocol + * The subprotocol this server supports. + */ + public WebSocketServer(int port, String origin, String subprotocol) { + this(port, origin, subprotocol, DEFAULT_DRAFT); + } + + /** + * Creates a WebSocketServer that will attempt to listen on port + * port using a specified subprotocol. Only allows + * connections from origin using specified draft of the + * WebSocket protocol. + * + * @param port + * The port number this server should listen on. + * @param origin + * The origin this server supports. + * @param subprotocol + * The subprotocol this server supports. + * @param draft + * The draft of the WebSocket protocol this server supports. + */ + public WebSocketServer(int port, String origin, String subprotocol, + String draft) { + this(port, origin, subprotocol, Draft.valueOf(draft)); + } + + /** + * Creates a WebSocketServer that will attempt to listen on port + * port using a specified subprotocol. Only allows + * connections from origin using specified draft of the + * WebSocket protocol. + * + * @param port + * The port number this server should listen on. + * @param origin + * The origin this server supports. + * @param subprotocol + * The subprotocol this server supports. + * @param draft + * The draft of the WebSocket protocol this server supports. + */ + public WebSocketServer(int port, String origin, String subprotocol, + Draft draft) { + this.connections = new CopyOnWriteArraySet(); + setPort(port); + setOrigin(origin); + setSubProtocol(subprotocol); + setDraft(draft); + } + + // PUBLIC INSTANCE METHODS ///////////////////////////////////////////////// + /** + * Sets this WebSocketClient draft. + * + * @param draft + */ + public void setDraft(Draft draft) { + this.draft = draft; + } + + /** + * Gets the draft that this WebSocketClient supports. + * + * @return The draft for this WebSocketClient. + */ + public Draft getDraft() { + return draft; + } + + /** + * Sets the origin that this WebSocketServer should allow connections from. + * + * @param origin + * The origin to allow connections from. + */ + public void setOrigin(String origin) { + this.origin = origin; + } + + /** + * Gets the origin that this WebSocketServer should allow connections from. + * + * @return The origin. + */ + public String getOrigin() { + return origin; + } + + /** + * Sets the port that this WebSocketServer should listen on. + * + * @param port + * The port number to listen on. + */ + public void setPort(int port) { + this.port = port; + } + + /** + * Gets the port number that this server listens on. + * + * @return The port number. + */ + public int getPort() { + return port; + } + + /** + * Sets this WebSocketClient subprotocol. + * + * @param subprotocol + */ + public void setSubProtocol(String subprotocol) { + this.subprotocol = subprotocol; + } + + /** + * Gets the subprotocol that this WebSocketClient supports. + * + * @return The subprotocol for this WebSocketClient. + */ + public String getSubProtocol() { + return subprotocol; + } + + /** + * Starts the server thread that binds to the currently set port number and + * listeners for WebSocket connection requests. + */ + public void start() { + (new Thread(this)).start(); + } + + /** + * Closes all connected clients sockets, then closes the underlying + * ServerSocketChannel, effectively killing the server socket thread and + * freeing the port the server was bound to. + * + * @throws IOException + * When socket related I/O errors occur. + */ + public void stop() throws IOException { + for (WebSocket ws : connections) { + ws.close(); } - - /** - * Creates a WebSocketServer that will attempt to listen on port - * port. - * @param port The port number this server should listen on. - */ - public WebSocketServer(int port) { - this(port,null,null,DEFAULT_DRAFT); + this.server.close(); + } + + /** + * Sends text to connected WebSocket client specified by + * connection. + * + * @param connection + * The {@link WebSocket} connection to send to. + * @param text + * The String to send to connection. + * @throws IOException + * When socket related I/O errors occur. + */ + public void sendTo(WebSocket connection, String text) throws IOException { + if (connection == null) { + throw new NullPointerException("'connection' cannot be null"); } - - /** - * Creates a WebSocketServer that will attempt to listen on port - * port. Only allows connections from origin. - * @param port The port number this server should listen on. - * @param origin The origin this server supports. - */ - public WebSocketServer(int port, String origin) { - this(port,origin,null,DEFAULT_DRAFT); + connection.send(text); + } + + /** + * Sends text to all currently connected WebSocket clients found in + * the Set connections. + * + * @param connections + * @param text + * @throws IOException + * When socket related I/O errors occur. + */ + public void sendTo(Set connections, String text) + throws IOException { + if (connections == null) { + throw new NullPointerException("'connections' cannot be null"); } - - /** - * Creates a WebSocketServer that will attempt to listen on port - * port using a specified subprotocol. - * Only allows connections from origin. - * @param port The port number this server should listen on. - * @param origin The origin this server supports. - * @param subprotocol The subprotocol this server supports. - */ - public WebSocketServer(int port, String origin, String subprotocol) { - this(port,origin,subprotocol,DEFAULT_DRAFT); + + for (WebSocket c : this.connections) { + if (connections.contains(c)) { + c.send(text); + } } - - /** - * Creates a WebSocketServer that will attempt to listen on port - * port using a specified subprotocol. - * Only allows connections from origin using specified - * draft of the WebSocket protocol. - * @param port The port number this server should listen on. - * @param origin The origin this server supports. - * @param subprotocol The subprotocol this server supports. - * @param draft The draft of the WebSocket protocol this server supports. - */ - public WebSocketServer(int port, String origin, String subprotocol, String draft) { - this(port,origin,subprotocol,Draft.valueOf(draft)); + } + + /** + * Sends text to all currently connected WebSocket clients. + * + * @param text + * The String to send across the network. + * @throws IOException + * When socket related I/O errors occur. + */ + public void sendToAll(String text) throws IOException { + for (WebSocket c : this.connections) { + c.send(text); } - - /** - * Creates a WebSocketServer that will attempt to listen on port - * port using a specified subprotocol. - * Only allows connections from origin using specified - * draft of the WebSocket protocol. - * @param port The port number this server should listen on. - * @param origin The origin this server supports. - * @param subprotocol The subprotocol this server supports. - * @param draft The draft of the WebSocket protocol this server supports. - */ - public WebSocketServer(int port, String origin, String subprotocol, Draft draft) { - this.connections = new CopyOnWriteArraySet(); - setPort(port); - setOrigin(origin); - setSubProtocol(subprotocol); - setDraft(draft); + } + + /** + * Sends text to all currently connected WebSocket clients, except + * for the specified connection. + * + * @param connection + * The {@link WebSocket} connection to ignore. + * @param text + * The String to send to every connection except + * connection. + * @throws IOException + * When socket related I/O errors occur. + */ + public void sendToAllExcept(WebSocket connection, String text) + throws IOException { + if (connection == null) { + throw new NullPointerException("'connection' cannot be null"); } - - - // PUBLIC INSTANCE METHODS ///////////////////////////////////////////////// - /** - * Sets this WebSocketClient draft. - * @param draft - */ - public void setDraft(Draft draft) { - this.draft = draft; + + for (WebSocket c : this.connections) { + if (!connection.equals(c)) { + c.send(text); + } } - - /** - * Gets the draft that this WebSocketClient supports. - * @return The draft for this WebSocketClient. - */ - public Draft getDraft() { - return draft; + } + + /** + * Sends text to all currently connected WebSocket clients, except + * for those found in the Set connections. + * + * @param connections + * @param text + * @throws IOException + * When socket related I/O errors occur. + */ + public void sendToAllExcept(Set connections, String text) + throws IOException { + if (connections == null) { + throw new NullPointerException("'connections' cannot be null"); } - - /** - * Sets the origin that this WebSocketServer should allow connections - * from. - * @param origin The origin to allow connections from. - */ - public void setOrigin(String origin) { - this.origin = origin; + + for (WebSocket c : this.connections) { + if (!connections.contains(c)) { + c.send(text); + } } - - /** - * Gets the origin that this WebSocketServer should allow connections - * from. - * @return The origin. - */ - public String getOrigin() { - return origin; + } + + /** + * Returns a WebSocket[] of currently connected clients. + * + * @return The currently connected clients in a WebSocket[]. + */ + public WebSocket[] connections() { + return this.connections.toArray(new WebSocket[0]); + } + + /** + * @return A BlockingQueue that should be used by a WebSocket to hold data + * that is waiting to be sent to the client. The default + * implementation returns an unbounded LinkedBlockingQueue, but you + * may choose to override this to return a bounded queue to protect + * against running out of memory. + */ + protected BlockingQueue newBufferQueue() { + return new LinkedBlockingQueue(); + } + + // Runnable IMPLEMENTATION ///////////////////////////////////////////////// + public void run() { + try { + server = ServerSocketChannel.open(); + server.configureBlocking(false); + server.socket().bind(new java.net.InetSocketAddress(port)); + + Selector selector = Selector.open(); + server.register(selector, server.validOps()); + + while (true) { + + try { + selector.select(100L); + Set keys = selector.selectedKeys(); + Iterator i = keys.iterator(); + + while (i.hasNext()) { + SelectionKey key = i.next(); + + // Remove the current key + i.remove(); + + // if isAcceptable == true + // then a client required a connection + if (key.isAcceptable()) { + SocketChannel client = server.accept(); + client.configureBlocking(false); + WebSocket c = new WebSocket(client, newBufferQueue(), this, + ClientServerType.SERVER); + client.register(selector, SelectionKey.OP_READ, c); + } + + // if isReadable == true + // then the server is ready to read + if (key.isReadable()) { + WebSocket conn = (WebSocket) key.attachment(); + conn.handleRead(); + } + + // if isWritable == true + // then we need to send the rest of the data to the client + if (key.isWritable()) { + WebSocket conn = (WebSocket) key.attachment(); + if (conn.handleWrite()) { + conn.socketChannel().register(selector, SelectionKey.OP_READ, + conn); + } + } + } + + for (WebSocket conn : this.connections) { + // We have to do this check here, and not in the thread that + // adds the buffered data to the WebSocket, because the + // Selector is not thread-safe, and can only be accessed + // by this thread. + if (conn.hasBufferedData()) { + conn.socketChannel().register(selector, + SelectionKey.OP_READ | SelectionKey.OP_WRITE, conn); + } + } + + } catch (IOException e) { + //e.printStackTrace(); + } catch (RuntimeException e) { + //e.printStackTrace(); + } + } + } catch (IOException e) { + //e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + //e.printStackTrace(); } - - /** - * Sets the port that this WebSocketServer should listen on. - * @param port The port number to listen on. - */ - public void setPort(int port) { - this.port = port; + } + + /** + * Gets the XML string that should be returned if a client requests a Flash + * security policy. + * + * The default implementation allows access from all remote domains, but only + * on the port that this WebSocketServer is listening on. + * + * This is specifically implemented for gitime's WebSocket client for Flash: + * http://github.com/gimite/web-socket-js + * + * @return An XML String that comforms to Flash's security policy. You MUST + * not need to include the null char at the end, it is appended + * automatically. + */ + protected String getFlashSecurityPolicy() { + return ""; + } + + // WebSocketListener IMPLEMENTATION //////////////////////////////////////// + /** + * Called by a {@link WebSocket} instance when a client connection has + * finished sending a handshake. This method verifies that the handshake is a + * valid WebSocket cliend request. Then sends a WebSocket server handshake if + * it is valid, or closes the connection if it is not. + * + * @param conn + * The {@link WebSocket} instance who's handshake has been recieved. + * @param handshake + * The entire UTF-8 decoded handshake from the connection. + * @return True if the client sent a valid WebSocket handshake and this server + * successfully sent a WebSocket server handshake, false otherwise. + * @throws IOException + * When socket related I/O errors occur. + */ + public boolean onHandshakeRecieved(WebSocket conn, + WebSocketHandshake handshake) throws IOException { + if (FLASH_POLICY_REQUEST.equals(handshake.toString())) { + String policy = getFlashSecurityPolicy() + "\0"; + conn.socketChannel() + .write(ByteBuffer.wrap(policy.getBytes(UTF8_CHARSET))); + return false; } - - /** - * Gets the port number that this server listens on. - * @return The port number. - */ - public int getPort() { - return port; + + if (handshake.getDraft() == Draft.DRAFT75 + && (this.draft == Draft.AUTO || this.draft == Draft.DRAFT75)) { + return handleHandshake75(conn, handshake); + } else if (handshake.getDraft() == Draft.DRAFT76 + && (this.draft == Draft.AUTO || this.draft == Draft.DRAFT76)) { + return handleHandshake76(conn, handshake); } - - /** - * Sets this WebSocketClient subprotocol. - * @param subprotocol - */ - public void setSubProtocol(String subprotocol) { - this.subprotocol = subprotocol; + + // If we get here we got a handshake that is incompatibile with the server. + return false; + } + + private boolean handleHandshake75(WebSocket conn, WebSocketHandshake handshake) + throws IOException { + + if (!handshake.getProperty("Method").equals("GET") + || !handshake.getProperty("HTTP-Version").equals("HTTP/1.1") + || !handshake.getProperty("Request-URI").startsWith("/")) { + return false; } - - /** - * Gets the subprotocol that this WebSocketClient supports. - * @return The subprotocol for this WebSocketClient. - */ - public String getSubProtocol() { - return subprotocol; + + String prop = handshake.getProperty("Upgrade"); + if (prop == null || !prop.equals("WebSocket")) { + return false; } - /** - * Starts the server thread that binds to the currently set port number and - * listeners for WebSocket connection requests. - */ - public void start() { - (new Thread(this)).start(); + prop = handshake.getProperty("Connection"); + if (prop == null || !prop.equals("Upgrade")) { + return false; } - /** - * Closes all connected clients sockets, then closes the underlying - * ServerSocketChannel, effectively killing the server socket thread and - * freeing the port the server was bound to. - * @throws IOException When socket related I/O errors occur. - */ - public void stop() throws IOException { - for (WebSocket ws : connections) - ws.close(); - this.server.close(); + if (subprotocol != null) { + prop = handshake.getProperty("Websocket-Protocol"); + if (prop == null || !prop.equals(subprotocol)) { + return false; + } } - /** - * Sends text to all currently connected WebSocket clients. - * @param text The String to send across the network. - * @throws IOException When socket related I/O errors occur. - */ - public void sendToAll(String text) throws IOException { - for (WebSocket c : this.connections) - c.send(text); + if (origin != null) { + prop = handshake.getProperty("Origin"); + if (prop == null || !prop.startsWith(origin)) { + return false; + } } - /** - * Sends text to all currently connected WebSocket clients, - * except for the specified connection. - * @param connection The {@link WebSocket} connection to ignore. - * @param text The String to send to every connection except connection. - * @throws IOException When socket related I/O errors occur. - */ - public void sendToAllExcept(WebSocket connection, String text) throws IOException { - if (connection == null) throw new NullPointerException("'connection' cannot be null"); - for (WebSocket c : this.connections) - if (!connection.equals(c)) - c.send(text); + // If we've determined that this is a valid WebSocket request, send a + // valid WebSocket server handshake, then return true to keep connection + // alive. + + WebSocketHandshake serverHandshake = new WebSocketHandshake(); + serverHandshake.setType(ClientServerType.SERVER); + serverHandshake.setDraft(Draft.DRAFT75); + serverHandshake.put("Host", handshake.getProperty("Host")); + serverHandshake.put("Request-URI", handshake.getProperty("Request-URI")); + serverHandshake.put("Origin", handshake.getProperty("Origin")); + if (handshake.containsKey("Websocket-Protocol")) { + serverHandshake.put("Websocket-Protocol", handshake + .getProperty("Websocket-Protocol")); } - /** - * Sends text to all currently connected WebSocket clients, - * except for those found in the Set connections. - * @param connections - * @param text - * @throws IOException When socket related I/O errors occur. - */ - public void sendToAllExcept(Set connections, String text) throws IOException { - if (connections == null) throw new NullPointerException("'connections' cannot be null"); - for (WebSocket c : this.connections) - if (!connections.contains(c)) - c.send(text); + conn.socketChannel().write(ByteBuffer.wrap(serverHandshake.getHandshake())); + + return true; + + } + + private boolean handleHandshake76(WebSocket conn, WebSocketHandshake handshake) + throws IOException { + + if (!handshake.getProperty("method").equals("GET") + || !handshake.getProperty("request-uri").matches("^/[\u0021-\u007E]*")) { + return false; } - /** - * Returns a WebSocket[] of currently connected clients. - * @return The currently connected clients in a WebSocket[]. - */ - public WebSocket[] connections() { - return this.connections.toArray(new WebSocket[0]); + String prop; + + prop = handshake.getProperty("upgrade"); + if (prop == null || !(prop.equalsIgnoreCase("WebSocket"))) { + return false; } + prop = handshake.getProperty("connection"); + if (prop == null || !(prop.equalsIgnoreCase("Upgrade"))) { + return false; + } - // Runnable IMPLEMENTATION ///////////////////////////////////////////////// - public void run() { - try { - server = ServerSocketChannel.open(); - server.configureBlocking(false); - server.socket().bind(new java.net.InetSocketAddress(port)); - - Selector selector = Selector.open(); - server.register(selector, server.validOps()); - - while(true) { - selector.select(); - Set keys = selector.selectedKeys(); - Iterator i = keys.iterator(); - - while(i.hasNext()) { - SelectionKey key = i.next(); - - // Remove the current key - i.remove(); - - // if isAccetable == true - // then a client required a connection - if (key.isAcceptable()) { - SocketChannel client = server.accept(); - client.configureBlocking(false); - WebSocket c = new WebSocket(client, this, ClientServerType.SERVER); - client.register(selector, SelectionKey.OP_READ, c); - } - - // if isReadable == true - // then the server is ready to read - if (key.isReadable()) { - WebSocket conn = (WebSocket)key.attachment(); - conn.handleRead(); - } - } - } - } catch (IOException ex) { - ex.printStackTrace(); - } - //System.out.println("End of WebSocketServer socket thread: " + Thread.currentThread()); + if (!handshake.containsKey("host")) { + return false; } - /** - * Gets the XML string that should be returned if a client requests a Flash - * security policy. - * - * The default implementation allows access from all remote domains, but - * only on the port that this WebSocketServer is listening on. - * - * This is specifically implemented for gitime's WebSocket client for Flash: - * http://github.com/gimite/web-socket-js - * - * @return An XML String that comforms to Flash's security policy. You MUST - * not need to include the null char at the end, it is appended - * automatically. - */ - protected String getFlashSecurityPolicy() { - return ""; + if (!handshake.containsKey("origin")) { + return false; } + if (origin != null) { + prop = handshake.getProperty("origin"); + if (prop == null || !prop.startsWith(origin)) { + return false; + } + } - // WebSocketListener IMPLEMENTATION //////////////////////////////////////// - /** - * Called by a {@link WebSocket} instance when a client connection has - * finished sending a handshake. This method verifies that the handshake is - * a valid WebSocket cliend request. Then sends a WebSocket server handshake - * if it is valid, or closes the connection if it is not. - * @param conn The {@link WebSocket} instance who's handshake has been recieved. - * @param handshake The entire UTF-8 decoded handshake from the connection. - * @return True if the client sent a valid WebSocket handshake and this server - * successfully sent a WebSocket server handshake, false otherwise. - * @throws IOException When socket related I/O errors occur. - */ - public boolean onHandshakeRecieved(WebSocket conn, WebSocketHandshake handshake) throws IOException { - if (FLASH_POLICY_REQUEST.equals(handshake.toString())) { - String policy = getFlashSecurityPolicy() + "\0"; - conn.socketChannel().write(ByteBuffer.wrap(policy.getBytes(UTF8_CHARSET))); - return false; - } - - if (handshake.getDraft() == Draft.DRAFT75 && (this.draft == Draft.AUTO || this.draft == Draft.DRAFT75)) { - return handleHandshake75(conn,handshake); - } else if (handshake.getDraft() == Draft.DRAFT76 && (this.draft == Draft.AUTO || this.draft == Draft.DRAFT76)) { - return handleHandshake76(conn,handshake); - } - - // If we get here we got a handshake that is incompatibile with the server. + if (subprotocol != null) { + prop = handshake.getProperty("sec-websocket-protocol"); + if (prop == null || !prop.equals(subprotocol)) { return false; + } } - private boolean handleHandshake75(WebSocket conn, WebSocketHandshake handshake) throws IOException { - - if (!handshake.getProperty("Method").equals("GET") - || !handshake.getProperty("HTTP-Version").equals("HTTP/1.1") - || !handshake.getProperty("Request-URI").startsWith("/")) { - return false; - } - - String prop = handshake.getProperty("Upgrade"); - if (prop == null || !prop.equals("WebSocket")) { - return false; - } - - prop = handshake.getProperty("Connection"); - if (prop == null || !prop.equals("Upgrade")) { - return false; - } - - if (subprotocol != null) { - prop = handshake.getProperty("Websocket-Protocol"); - if (prop == null || !prop.equals(subprotocol)) { - return false; - } - } - - if (origin != null) { - prop = handshake.getProperty("Origin"); - if (prop == null || !prop.startsWith(origin)) { - return false; - } - } - - // If we've determined that this is a valid WebSocket request, send a - // valid WebSocket server handshake, then return true to keep connection alive. - - WebSocketHandshake serverHandshake = new WebSocketHandshake(); - serverHandshake.setType(ClientServerType.SERVER); - serverHandshake.setDraft(Draft.DRAFT75); - serverHandshake.put("Host", handshake.getProperty("Host")); - serverHandshake.put("Request-URI", handshake.getProperty("Request-URI")); - serverHandshake.put("Origin", handshake.getProperty("Origin")); - if (handshake.containsKey("Websocket-Protocol")) { - serverHandshake.put("Websocket-Protocol", handshake.getProperty("Websocket-Protocol")); - } - - conn.socketChannel().write(ByteBuffer.wrap(serverHandshake.getHandshake())); - - return true; - + if (!handshake.containsKey("sec-websocket-key1") + || !handshake.containsKey("sec-websocket-key2")) { + return false; } - - private boolean handleHandshake76(WebSocket conn, WebSocketHandshake handshake) throws IOException { - - if (!handshake.getProperty("method").equals("GET") - || !handshake.getProperty("request-uri").matches("^/[\u0021-\u007E]*")) { - return false; - } - - String prop; - - prop = handshake.getProperty("upgrade"); - if (prop == null || !(prop.equalsIgnoreCase("WebSocket"))) { - return false; - } - - prop = handshake.getProperty("connection"); - if (prop == null || !(prop.equalsIgnoreCase("Upgrade"))) { - return false; - } - - if (!handshake.containsKey("host")) { - return false; - } - - - if (!handshake.containsKey("origin")) { - return false; - } - - if (origin != null) { - prop = handshake.getProperty("origin"); - if (prop == null || !prop.startsWith(origin)) { - return false; - } - } - - if (subprotocol != null) { - prop = handshake.getProperty("sec-websocket-protocol"); - if (prop == null || !prop.equals(subprotocol)) { - return false; - } - } - - if (!handshake.containsKey("sec-websocket-key1") || !handshake.containsKey("sec-websocket-key2")) { - return false; - } - - byte[] key3 = handshake.getAsByteArray("key3"); - - long key1 = Long.parseLong(handshake.getProperty("sec-websocket-key1").replaceAll("[^0-9]","")); - long key2 = Long.parseLong(handshake.getProperty("sec-websocket-key2").replaceAll("[^0-9]","")); - - if (key1 > MAX_KEY_VALUE || key2 > MAX_KEY_VALUE) { - return false; - } - - int spaces1 = handshake.getProperty("sec-websocket-key1").replaceAll("[^ ]", "").length(); - int spaces2 = handshake.getProperty("sec-websocket-key2").replaceAll("[^ ]", "").length(); - - if (spaces1 == 0 || spaces2 == 0) { - return false; - } - - long mod1 = key1 % spaces1; - long mod2 = key2 % spaces2; - - if (mod1 != 0 || mod2 != 0) { - return false; - } - - long part1 = key1/spaces1; - long part2 = key2/spaces2; - - byte[] challenge = { - (byte)(part1 >> 24), - (byte)(part1 >> 16), - (byte)(part1 >> 8), - (byte)(part1 >> 0), - (byte)(part2 >> 24), - (byte)(part2 >> 16), - (byte)(part2 >> 8), - (byte)(part2 >> 0), - (byte) key3[0], - (byte) key3[1], - (byte) key3[2], - (byte) key3[3], - (byte) key3[4], - (byte) key3[5], - (byte) key3[6], - (byte) key3[7] - }; - - byte[] response; - - try { - MessageDigest md = MessageDigest.getInstance("MD5"); - response = md.digest(challenge); - } catch (NoSuchAlgorithmException e) { - return false; - } - - WebSocketHandshake serverHandshake = new WebSocketHandshake(); - serverHandshake.setType(ClientServerType.SERVER); - serverHandshake.setDraft(Draft.DRAFT76); - serverHandshake.put("host", handshake.getProperty("host")); - serverHandshake.put("request-uri", handshake.getProperty("request-uri")); - serverHandshake.put("origin", handshake.getProperty("origin")); - serverHandshake.put("response", response); - if (handshake.containsKey("sec-websocket-protocol")) { - serverHandshake.put("sec-websocket-protocol", handshake.getProperty("sec-websocket-protocol")); - } - - conn.socketChannel().write(ByteBuffer.wrap(serverHandshake.getHandshake())); - - return true; - + + byte[] key3 = handshake.getAsByteArray("key3"); + + long key1 = Long.parseLong(handshake.getProperty("sec-websocket-key1") + .replaceAll("[^0-9]", "")); + long key2 = Long.parseLong(handshake.getProperty("sec-websocket-key2") + .replaceAll("[^0-9]", "")); + + if (key1 > MAX_KEY_VALUE || key2 > MAX_KEY_VALUE) { + return false; } - - public void onMessage(WebSocket conn, String message) { - onClientMessage(conn, message); + + int spaces1 = handshake.getProperty("sec-websocket-key1").replaceAll( + "[^ ]", "").length(); + int spaces2 = handshake.getProperty("sec-websocket-key2").replaceAll( + "[^ ]", "").length(); + + if (spaces1 == 0 || spaces2 == 0) { + return false; } - public void onOpen(WebSocket conn) { - if (this.connections.add(conn)) - onClientOpen(conn); + long mod1 = key1 % spaces1; + long mod2 = key2 % spaces2; + + if (mod1 != 0 || mod2 != 0) { + return false; } - - public void onClose(WebSocket conn) { - if (this.connections.remove(conn)) - onClientClose(conn); + + long part1 = key1 / spaces1; + long part2 = key2 / spaces2; + + byte[] challenge = { + (byte) (part1 >> 24), + (byte) (part1 >> 16), + (byte) (part1 >> 8), + (byte) (part1 >> 0), + (byte) (part2 >> 24), + (byte) (part2 >> 16), + (byte) (part2 >> 8), + (byte) (part2 >> 0), + (byte) key3[0], + (byte) key3[1], + (byte) key3[2], + (byte) key3[3], + (byte) key3[4], + (byte) key3[5], + (byte) key3[6], + (byte) key3[7] }; + + byte[] response; + + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + response = md.digest(challenge); + } catch (NoSuchAlgorithmException e) { + return false; + } + + WebSocketHandshake serverHandshake = new WebSocketHandshake(); + serverHandshake.setType(ClientServerType.SERVER); + serverHandshake.setDraft(Draft.DRAFT76); + serverHandshake.put("host", handshake.getProperty("host")); + serverHandshake.put("request-uri", handshake.getProperty("request-uri")); + serverHandshake.put("origin", handshake.getProperty("origin")); + serverHandshake.put("response", response); + if (handshake.containsKey("sec-websocket-protocol")) { + serverHandshake.put("sec-websocket-protocol", handshake + .getProperty("sec-websocket-protocol")); } - // ABTRACT METHODS ///////////////////////////////////////////////////////// - public abstract void onClientOpen(WebSocket conn); - public abstract void onClientClose(WebSocket conn); - public abstract void onClientMessage(WebSocket conn, String message); + conn.socketChannel().write(ByteBuffer.wrap(serverHandshake.getHandshake())); + + return true; + + } + + public void onMessage(WebSocket conn, String message) { + onClientMessage(conn, message); + } + + public void onOpen(WebSocket conn) { + if (this.connections.add(conn)) + onClientOpen(conn); + } + + public void onClose(WebSocket conn) { + if (this.connections.remove(conn)) + onClientClose(conn); + } + + // ABTRACT METHODS ///////////////////////////////////////////////////////// + public abstract void onClientOpen(WebSocket conn); + public abstract void onClientClose(WebSocket conn); + public abstract void onClientMessage(WebSocket conn, String message); } From 37fb538807181502d3c2e908494c08ea3c6b5377 Mon Sep 17 00:00:00 2001 From: Nathan Mische Date: Wed, 22 Sep 2010 10:17:11 -0400 Subject: [PATCH 11/12] Adding continue statements to server loop. --- src/WebSocketServer.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/WebSocketServer.java b/src/WebSocketServer.java index 121639d33..38eda63c4 100644 --- a/src/WebSocketServer.java +++ b/src/WebSocketServer.java @@ -397,7 +397,7 @@ public void run() { while (true) { try { - selector.select(100L); + selector.select(); Set keys = selector.selectedKeys(); Iterator i = keys.iterator(); @@ -415,6 +415,7 @@ public void run() { WebSocket c = new WebSocket(client, newBufferQueue(), this, ClientServerType.SERVER); client.register(selector, SelectionKey.OP_READ, c); + continue; } // if isReadable == true @@ -422,6 +423,7 @@ public void run() { if (key.isReadable()) { WebSocket conn = (WebSocket) key.attachment(); conn.handleRead(); + continue; } // if isWritable == true @@ -432,6 +434,7 @@ public void run() { conn.socketChannel().register(selector, SelectionKey.OP_READ, conn); } + continue; } } @@ -447,15 +450,15 @@ public void run() { } } catch (IOException e) { - //e.printStackTrace(); + e.printStackTrace(); } catch (RuntimeException e) { - //e.printStackTrace(); + e.printStackTrace(); } } } catch (IOException e) { - //e.printStackTrace(); + e.printStackTrace(); } catch (NoSuchAlgorithmException e) { - //e.printStackTrace(); + e.printStackTrace(); } } From 84467d358bd6ab1f02e00f3caa8f849204657d29 Mon Sep 17 00:00:00 2001 From: Nathan Mische Date: Wed, 22 Sep 2010 10:54:38 -0400 Subject: [PATCH 12/12] Moving to net.tootallnate.websocket package. Updating example and docs. --- README.markdown | 45 +- build.xml | 22 + doc/WebSocketListener.html | 349 ----- doc/WebSocketProtocol.ClientServerType.html | 324 ----- doc/WebSocketProtocol.Draft.html | 340 ----- doc/WebSocketProtocol.html | 260 ---- doc/allclasses-frame.html | 20 +- doc/allclasses-noframe.html | 20 +- doc/class-use/WebSocket.html | 332 ----- doc/class-use/WebSocketClient.html | 142 -- doc/class-use/WebSocketHandshake.html | 187 --- doc/class-use/WebSocketListener.html | 191 --- .../WebSocketProtocol.ClientServerType.html | 222 --- doc/class-use/WebSocketProtocol.Draft.html | 273 ---- doc/class-use/WebSocketProtocol.html | 207 --- doc/class-use/WebSocketServer.html | 142 -- doc/constant-values.html | 52 +- doc/deprecated-list.html | 18 +- doc/help-doc.html | 24 +- doc/index-all.html | 471 +++++++ doc/index-files/index-1.html | 144 -- doc/index-files/index-10.html | 198 --- doc/index-files/index-11.html | 143 -- doc/index-files/index-12.html | 146 -- doc/index-files/index-13.html | 203 --- doc/index-files/index-14.html | 143 -- doc/index-files/index-15.html | 143 -- doc/index-files/index-16.html | 154 --- doc/index-files/index-17.html | 192 --- doc/index-files/index-2.html | 158 --- doc/index-files/index-3.html | 148 -- doc/index-files/index-4.html | 143 -- doc/index-files/index-5.html | 144 -- doc/index-files/index-6.html | 182 --- doc/index-files/index-7.html | 144 -- doc/index-files/index-8.html | 143 -- doc/index-files/index-9.html | 143 -- doc/index.html | 6 +- .../tootallnate/websocket}/WebSocket.html | 222 +-- .../websocket}/WebSocketClient.html | 275 ++-- .../websocket}/WebSocketHandshake.html | 218 +-- .../websocket}/WebSocketServer.html | 447 +++--- .../tootallnate/websocket/package-frame.html | 38 + .../websocket/package-summary.html | 168 +++ .../tootallnate/websocket/package-tree.html | 153 +++ doc/overview-tree.html | 42 +- doc/package-frame.html | 64 - doc/package-list | 2 +- doc/package-summary.html | 195 --- doc/package-tree.html | 174 --- doc/package-use.html | 155 --- doc/serialized-form.html | 32 +- example/ChatClient.java | 11 +- example/ChatServer.java | 6 +- example/FABridge.js | 1208 ++++++++--------- example/WebSocketMain.swf | Bin 11780 -> 180134 bytes example/chat.html | 22 +- example/web_socket.js | 530 +++++--- .../tootallnate/websocket}/WebSocket.java | 1 + .../websocket}/WebSocketClient.java | 16 +- .../websocket}/WebSocketHandshake.java | 1 + .../websocket}/WebSocketListener.java | 1 + .../websocket}/WebSocketProtocol.java | 1 + .../websocket}/WebSocketServer.java | 1 + 64 files changed, 2687 insertions(+), 7614 deletions(-) create mode 100644 build.xml delete mode 100644 doc/WebSocketListener.html delete mode 100644 doc/WebSocketProtocol.ClientServerType.html delete mode 100644 doc/WebSocketProtocol.Draft.html delete mode 100644 doc/WebSocketProtocol.html delete mode 100644 doc/class-use/WebSocket.html delete mode 100644 doc/class-use/WebSocketClient.html delete mode 100644 doc/class-use/WebSocketHandshake.html delete mode 100644 doc/class-use/WebSocketListener.html delete mode 100644 doc/class-use/WebSocketProtocol.ClientServerType.html delete mode 100644 doc/class-use/WebSocketProtocol.Draft.html delete mode 100644 doc/class-use/WebSocketProtocol.html delete mode 100644 doc/class-use/WebSocketServer.html create mode 100644 doc/index-all.html delete mode 100644 doc/index-files/index-1.html delete mode 100644 doc/index-files/index-10.html delete mode 100644 doc/index-files/index-11.html delete mode 100644 doc/index-files/index-12.html delete mode 100644 doc/index-files/index-13.html delete mode 100644 doc/index-files/index-14.html delete mode 100644 doc/index-files/index-15.html delete mode 100644 doc/index-files/index-16.html delete mode 100644 doc/index-files/index-17.html delete mode 100644 doc/index-files/index-2.html delete mode 100644 doc/index-files/index-3.html delete mode 100644 doc/index-files/index-4.html delete mode 100644 doc/index-files/index-5.html delete mode 100644 doc/index-files/index-6.html delete mode 100644 doc/index-files/index-7.html delete mode 100644 doc/index-files/index-8.html delete mode 100644 doc/index-files/index-9.html rename doc/{ => net/tootallnate/websocket}/WebSocket.html (53%) rename doc/{ => net/tootallnate/websocket}/WebSocketClient.html (55%) rename doc/{ => net/tootallnate/websocket}/WebSocketHandshake.html (50%) rename doc/{ => net/tootallnate/websocket}/WebSocketServer.html (54%) create mode 100644 doc/net/tootallnate/websocket/package-frame.html create mode 100644 doc/net/tootallnate/websocket/package-summary.html create mode 100644 doc/net/tootallnate/websocket/package-tree.html delete mode 100644 doc/package-frame.html delete mode 100644 doc/package-summary.html delete mode 100644 doc/package-tree.html delete mode 100644 doc/package-use.html rename src/{ => net/tootallnate/websocket}/WebSocket.java (99%) rename src/{ => net/tootallnate/websocket}/WebSocketClient.java (96%) rename src/{ => net/tootallnate/websocket}/WebSocketHandshake.java (99%) rename src/{ => net/tootallnate/websocket}/WebSocketListener.java (98%) rename src/{ => net/tootallnate/websocket}/WebSocketProtocol.java (94%) rename src/{ => net/tootallnate/websocket}/WebSocketServer.java (99%) diff --git a/README.markdown b/README.markdown index 9259f5739..d749a8e68 100644 --- a/README.markdown +++ b/README.markdown @@ -1,48 +1,53 @@ Java WebSockets =============== -This repository contains a simple WebSocket server and client implementation -in Java. The underlying classes use the Java classes `ServerSocketChannel` and -`SocketChannel`, to allow for a non-blocking event-driven model (similar to the +This repository contains a barebones WebSocket server and client implementation +written in 100% Java. The implementation supports both the older draft 75, +and current draft 76. The underlying classes use the Java +`ServerSocketChannel` and `SocketChannel` classes, which allows for a +non-blocking event-driven model (similar to the [WebSocket API]() for web browsers). Running the Example ------------------- There's a simple chat server and client example located in the `example` -folder. +folder. First, compile the example classes and JAR file: + ant -First, start the chat server (a WebSocketServer subclass): - cd example/ - java ChatServer +Then, start the chat server (a `WebSocketServer` subclass): + java -cp example:dist/WebSocket.jar ChatServer Now that the server is started, we need to connect some clients. Run the -Java chat client (a WebSocketClient subclass): - java ChatClient +Java chat client (a `WebSocketClient` subclass): + java -cp example:dist/WebSocket.jar ChatClient -The chat client is a simple Swing GUI that allows you to send messages to -all other connected clients, and recieve messages from others in a text box. +The chat client is a simple Swing GUI application that allows you to send +messages to all other connected clients, and receive messages from others in a +text box. There's also a simple HTML file chat client `chat.html`, which can be opened -by any browser. If the browser natively supports the WebSocket API, then it will -be used, otherwise it will fall back to a +by any browser. If the browser natively supports the WebSocket API, then it's +implementation will be used, otherwise it will fall back to a [Flash-based WebSocket Implementation](). Writing your own WebSocket Server --------------------------------- +The `net.tootallnate.websocket.WebSocketServer` abstract class implements the +server-side of the +[WebSocket Protocol](). A WebSocket server by itself doesn't do anything except establish socket -connections though HTTP. After that it's up to a subclass to add purpose. -The `WebSocketServer` class implements the server-side of the -[WebSocket Protocol](). +connections though HTTP. After that it's up to **your** subclass to add purpose. Writing your own WebSocket Client --------------------------------- -The `WebSocketClient` class can connect to valid WebSocket servers. -The constructor expects a valid `ws://` URI to connect to. Important -events `onOpen`, `onClose`, and `onMessage` get fired throughout the life -of the WebSocketClient, and must be implemented in your subclass. +The `net.tootallnate.websocket.WebSocketClient` abstract class can connect to +valid WebSocket servers. The constructor expects a valid `ws://` URI to +connect to. Important events `onOpen`, `onClose`, and `onMessage` get fired +throughout the life of the WebSocketClient, and must be implemented in **your** +subclass. License ------- diff --git a/build.xml b/build.xml new file mode 100644 index 000000000..431e3358c --- /dev/null +++ b/build.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/doc/WebSocketListener.html b/doc/WebSocketListener.html deleted file mode 100644 index 98b48d11a..000000000 --- a/doc/WebSocketListener.html +++ /dev/null @@ -1,349 +0,0 @@ - - - - - - -WebSocketListener - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - - - - - -
    - -
    - - - -
    - -

    -Interface WebSocketListener

    -
    -
    All Superinterfaces:
    WebSocketProtocol
    -
    -
    -
    All Known Implementing Classes:
    WebSocketClient, WebSocketServer
    -
    -
    -
    -
    interface WebSocketListener
    extends WebSocketProtocol
    - - -

    -Implemented by WebSocketClient and WebSocketServer. - The methods within are called by WebSocket. -

    - -

    -

    -
    Author:
    -
    Nathan Rajlich
    -
    -
    - -

    - - - - - - - -
    -Nested Class Summary
    - - - - - - - -
    Nested classes/interfaces inherited from interface WebSocketProtocol
    WebSocketProtocol.ClientServerType, WebSocketProtocol.Draft
    -  - - - - - - - -
    -Field Summary
    - - - - - - - -
    Fields inherited from interface WebSocketProtocol
    MAX_KEY_VALUE, UTF8_CHARSET
    -  - - - - - - - - - - - - - - - - - - - - - - - -
    -Method Summary
    - voidonClose(WebSocket conn) - -
    -          Called after WebSocket#close is explicity called, or when the - other end of the WebSocket connection is closed.
    - booleanonHandshakeRecieved(WebSocket conn, - WebSocketHandshake handshake) - -
    -          Called when the socket connection is first established, and the WebSocket - handshake has been recieved.
    - voidonMessage(WebSocket conn, - java.lang.String message) - -
    -          Called when an entire text frame has been recieved.
    - voidonOpen(WebSocket conn) - -
    -          Called after onHandshakeRecieved returns true.
    -  -

    - - - - - - - - -
    -Method Detail
    - -

    -onHandshakeRecieved

    -
    -boolean onHandshakeRecieved(WebSocket conn,
    -                            WebSocketHandshake handshake)
    -                            throws java.io.IOException
    -
    -
    Called when the socket connection is first established, and the WebSocket - handshake has been recieved. This method should parse the - handshake, and return a boolean indicating whether or not the - connection is a valid WebSocket connection. -

    -

    -
    -
    -
    -
    Parameters:
    conn - The WebSocket instance this event is occuring on.
    handshake - The WebSocketHandshake. -
    Returns:
    true if the handshake is valid, and onOpen - should be immediately called afterwards. false if the - handshake was invalid, and the connection should be terminated. -
    Throws: -
    java.io.IOException
    -
    -
    -
    - -

    -onMessage

    -
    -void onMessage(WebSocket conn,
    -               java.lang.String message)
    -
    -
    Called when an entire text frame has been recieved. Do whatever you want - here... -

    -

    -
    -
    -
    -
    Parameters:
    conn - The WebSocket instance this event is occuring on.
    message - The UTF-8 decoded message that was recieved.
    -
    -
    -
    - -

    -onOpen

    -
    -void onOpen(WebSocket conn)
    -
    -
    Called after onHandshakeRecieved returns true. - Indicates that a complete WebSocket connection has been established, - and we are ready to send/recieve data. -

    -

    -
    -
    -
    -
    Parameters:
    conn - The WebSocket instance this event is occuring on.
    -
    -
    -
    - -

    -onClose

    -
    -void onClose(WebSocket conn)
    -
    -
    Called after WebSocket#close is explicity called, or when the - other end of the WebSocket connection is closed. -

    -

    -
    -
    -
    -
    Parameters:
    conn - The WebSocket instance this event is occuring on.
    -
    -
    - -
    - - - - - - - - - - - - - - - - - - - -
    - -
    - - - -
    - - - diff --git a/doc/WebSocketProtocol.ClientServerType.html b/doc/WebSocketProtocol.ClientServerType.html deleted file mode 100644 index b93d8f930..000000000 --- a/doc/WebSocketProtocol.ClientServerType.html +++ /dev/null @@ -1,324 +0,0 @@ - - - - - - -WebSocketProtocol.ClientServerType - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - - - - - -
    - -
    - - - -
    - -

    -Enum WebSocketProtocol.ClientServerType

    -
    -java.lang.Object
    -  extended by java.lang.Enum<WebSocketProtocol.ClientServerType>
    -      extended by WebSocketProtocol.ClientServerType
    -
    -
    -
    All Implemented Interfaces:
    java.io.Serializable, java.lang.Comparable<WebSocketProtocol.ClientServerType>
    -
    -
    -
    Enclosing interface:
    WebSocketProtocol
    -
    -
    -
    -
    public static enum WebSocketProtocol.ClientServerType
    extends java.lang.Enum<WebSocketProtocol.ClientServerType>
    - - -

    -The type of WebSocket -

    - -

    -


    - -

    - - - - - - - - - - - - - -
    -Enum Constant Summary
    CLIENT - -
    -           
    SERVER - -
    -           
    -  - - - - - - - - - - - - - - - -
    -Method Summary
    -static WebSocketProtocol.ClientServerTypevalueOf(java.lang.String name) - -
    -          Returns the enum constant of this type with the specified name.
    -static WebSocketProtocol.ClientServerType[]values() - -
    -          Returns an array containing the constants of this enum type, in -the order they are declared.
    - - - - - - - -
    Methods inherited from class java.lang.Enum
    clone, compareTo, equals, finalize, getDeclaringClass, hashCode, name, ordinal, toString, valueOf
    - - - - - - - -
    Methods inherited from class java.lang.Object
    getClass, notify, notifyAll, wait, wait, wait
    -  -

    - - - - - - - - -
    -Enum Constant Detail
    - -

    -CLIENT

    -
    -public static final WebSocketProtocol.ClientServerType CLIENT
    -
    -
    -
    -
    -
    - -

    -SERVER

    -
    -public static final WebSocketProtocol.ClientServerType SERVER
    -
    -
    -
    -
    - - - - - - - - -
    -Method Detail
    - -

    -values

    -
    -public static WebSocketProtocol.ClientServerType[] values()
    -
    -
    Returns an array containing the constants of this enum type, in -the order they are declared. This method may be used to iterate -over the constants as follows: -
    -for (WebSocketProtocol.ClientServerType c : WebSocketProtocol.ClientServerType.values())
    -    System.out.println(c);
    -
    -

    -

    - -
    Returns:
    an array containing the constants of this enum type, in -the order they are declared
    -
    -
    -
    - -

    -valueOf

    -
    -public static WebSocketProtocol.ClientServerType valueOf(java.lang.String name)
    -
    -
    Returns the enum constant of this type with the specified name. -The string must match exactly an identifier used to declare an -enum constant in this type. (Extraneous whitespace characters are -not permitted.) -

    -

    -
    Parameters:
    name - the name of the enum constant to be returned. -
    Returns:
    the enum constant with the specified name -
    Throws: -
    java.lang.IllegalArgumentException - if this enum type has no constant -with the specified name -
    java.lang.NullPointerException - if the argument is null
    -
    -
    - -
    - - - - - - - - - - - - - - - - - - - -
    - -
    - - - -
    - - - diff --git a/doc/WebSocketProtocol.Draft.html b/doc/WebSocketProtocol.Draft.html deleted file mode 100644 index 868e3839b..000000000 --- a/doc/WebSocketProtocol.Draft.html +++ /dev/null @@ -1,340 +0,0 @@ - - - - - - -WebSocketProtocol.Draft - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - - - - - -
    - -
    - - - -
    - -

    -Enum WebSocketProtocol.Draft

    -
    -java.lang.Object
    -  extended by java.lang.Enum<WebSocketProtocol.Draft>
    -      extended by WebSocketProtocol.Draft
    -
    -
    -
    All Implemented Interfaces:
    java.io.Serializable, java.lang.Comparable<WebSocketProtocol.Draft>
    -
    -
    -
    Enclosing interface:
    WebSocketProtocol
    -
    -
    -
    -
    public static enum WebSocketProtocol.Draft
    extends java.lang.Enum<WebSocketProtocol.Draft>
    - - -

    -The version of the WebSocket Internet-Draft -

    - -

    -


    - -

    - - - - - - - - - - - - - - - - -
    -Enum Constant Summary
    AUTO - -
    -           
    DRAFT75 - -
    -           
    DRAFT76 - -
    -           
    -  - - - - - - - - - - - - - - - -
    -Method Summary
    -static WebSocketProtocol.DraftvalueOf(java.lang.String name) - -
    -          Returns the enum constant of this type with the specified name.
    -static WebSocketProtocol.Draft[]values() - -
    -          Returns an array containing the constants of this enum type, in -the order they are declared.
    - - - - - - - -
    Methods inherited from class java.lang.Enum
    clone, compareTo, equals, finalize, getDeclaringClass, hashCode, name, ordinal, toString, valueOf
    - - - - - - - -
    Methods inherited from class java.lang.Object
    getClass, notify, notifyAll, wait, wait, wait
    -  -

    - - - - - - - - -
    -Enum Constant Detail
    - -

    -AUTO

    -
    -public static final WebSocketProtocol.Draft AUTO
    -
    -
    -
    -
    -
    - -

    -DRAFT75

    -
    -public static final WebSocketProtocol.Draft DRAFT75
    -
    -
    -
    -
    -
    - -

    -DRAFT76

    -
    -public static final WebSocketProtocol.Draft DRAFT76
    -
    -
    -
    -
    - - - - - - - - -
    -Method Detail
    - -

    -values

    -
    -public static WebSocketProtocol.Draft[] values()
    -
    -
    Returns an array containing the constants of this enum type, in -the order they are declared. This method may be used to iterate -over the constants as follows: -
    -for (WebSocketProtocol.Draft c : WebSocketProtocol.Draft.values())
    -    System.out.println(c);
    -
    -

    -

    - -
    Returns:
    an array containing the constants of this enum type, in -the order they are declared
    -
    -
    -
    - -

    -valueOf

    -
    -public static WebSocketProtocol.Draft valueOf(java.lang.String name)
    -
    -
    Returns the enum constant of this type with the specified name. -The string must match exactly an identifier used to declare an -enum constant in this type. (Extraneous whitespace characters are -not permitted.) -

    -

    -
    Parameters:
    name - the name of the enum constant to be returned. -
    Returns:
    the enum constant with the specified name -
    Throws: -
    java.lang.IllegalArgumentException - if this enum type has no constant -with the specified name -
    java.lang.NullPointerException - if the argument is null
    -
    -
    - -
    - - - - - - - - - - - - - - - - - - - -
    - -
    - - - -
    - - - diff --git a/doc/WebSocketProtocol.html b/doc/WebSocketProtocol.html deleted file mode 100644 index 44835d20b..000000000 --- a/doc/WebSocketProtocol.html +++ /dev/null @@ -1,260 +0,0 @@ - - - - - - -WebSocketProtocol - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - - - - - -
    - -
    - - - -
    - -

    -Interface WebSocketProtocol

    -
    -
    All Known Subinterfaces:
    WebSocketListener
    -
    -
    -
    All Known Implementing Classes:
    WebSocket, WebSocketClient, WebSocketHandshake, WebSocketServer
    -
    -
    -
    -
    interface WebSocketProtocol
    - - -

    -Constants used by WebSocket protocol. -

    - -

    -

    -
    Author:
    -
    Nathan Mische
    -
    -
    - -

    - - - - - - - - - - - - - - - -
    -Nested Class Summary
    -static classWebSocketProtocol.ClientServerType - -
    -          The type of WebSocket
    -static classWebSocketProtocol.Draft - -
    -          The version of the WebSocket Internet-Draft
    - - - - - - - - - - - - - - -
    -Field Summary
    -static java.lang.LongMAX_KEY_VALUE - -
    -          The maximum value of a WebSocket key.
    -static java.nio.charset.CharsetUTF8_CHARSET - -
    -          The WebSocket protocol expects UTF-8 encoded bytes.
    -  -

    - - - - - - - - -
    -Field Detail
    - -

    -MAX_KEY_VALUE

    -
    -static final java.lang.Long MAX_KEY_VALUE
    -
    -
    The maximum value of a WebSocket key. -

    -

    -
    -
    -
    - -

    -UTF8_CHARSET

    -
    -static final java.nio.charset.Charset UTF8_CHARSET
    -
    -
    The WebSocket protocol expects UTF-8 encoded bytes. -

    -

    -
    -
    - -
    - - - - - - - - - - - - - - - - - - - -
    - -
    - - - -
    - - - diff --git a/doc/allclasses-frame.html b/doc/allclasses-frame.html index e93699c8e..684d40d93 100644 --- a/doc/allclasses-frame.html +++ b/doc/allclasses-frame.html @@ -2,12 +2,12 @@ - + All Classes - + @@ -21,21 +21,13 @@ - diff --git a/doc/allclasses-noframe.html b/doc/allclasses-noframe.html index 672b838c8..25d7cc301 100644 --- a/doc/allclasses-noframe.html +++ b/doc/allclasses-noframe.html @@ -2,12 +2,12 @@ - + All Classes - + @@ -21,21 +21,13 @@
    WebSocket +WebSocket
    -WebSocketClient +WebSocketClient
    -WebSocketHandshake +WebSocketHandshake
    -WebSocketListener -
    -WebSocketProtocol -
    -WebSocketProtocol.ClientServerType -
    -WebSocketProtocol.Draft -
    -WebSocketServer +WebSocketServer
    - diff --git a/doc/class-use/WebSocket.html b/doc/class-use/WebSocket.html deleted file mode 100644 index 62cdf9173..000000000 --- a/doc/class-use/WebSocket.html +++ /dev/null @@ -1,332 +0,0 @@ - - - - - - -Uses of Class WebSocket - - - - - - - - - - - - -
    - - - - - -
    WebSocket +WebSocket
    -WebSocketClient +WebSocketClient
    -WebSocketHandshake +WebSocketHandshake
    -WebSocketListener -
    -WebSocketProtocol -
    -WebSocketProtocol.ClientServerType -
    -WebSocketProtocol.Draft -
    -WebSocketServer +WebSocketServer
    - - - - - - - - - -
    - -
    - - - -
    -
    -

    -Uses of Class
    WebSocket

    -
    - - - - - -
    -Uses of WebSocket in <Unnamed>
    -  -

    - - - - - - - - - -
    Methods in <Unnamed> that return WebSocket
    - WebSocket[]WebSocketServer.connections() - -
    -          Returns a WebSocket[] of currently connected clients.
    -  -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Methods in <Unnamed> with parameters of type WebSocket
    -abstract  voidWebSocketServer.onClientClose(WebSocket conn) - -
    -           
    -abstract  voidWebSocketServer.onClientMessage(WebSocket conn, - java.lang.String message) - -
    -           
    -abstract  voidWebSocketServer.onClientOpen(WebSocket conn) - -
    -           
    - voidWebSocketServer.onClose(WebSocket conn) - -
    -           
    - voidWebSocketClient.onClose(WebSocket conn) - -
    -          Calls subclass' implementation of onClose.
    - voidWebSocketListener.onClose(WebSocket conn) - -
    -          Called after WebSocket#close is explicity called, or when the - other end of the WebSocket connection is closed.
    - booleanWebSocketServer.onHandshakeRecieved(WebSocket conn, - WebSocketHandshake handshake) - -
    -          Called by a WebSocket instance when a client connection has - finished sending a handshake.
    - booleanWebSocketClient.onHandshakeRecieved(WebSocket conn, - WebSocketHandshake handshake) - -
    -          Parses the server's handshake to verify that it's a valid WebSocket - handshake.
    - booleanWebSocketListener.onHandshakeRecieved(WebSocket conn, - WebSocketHandshake handshake) - -
    -          Called when the socket connection is first established, and the WebSocket - handshake has been recieved.
    - voidWebSocketServer.onMessage(WebSocket conn, - java.lang.String message) - -
    -           
    - voidWebSocketClient.onMessage(WebSocket conn, - java.lang.String message) - -
    -          Calls subclass' implementation of onMessage.
    - voidWebSocketListener.onMessage(WebSocket conn, - java.lang.String message) - -
    -          Called when an entire text frame has been recieved.
    - voidWebSocketServer.onOpen(WebSocket conn) - -
    -           
    - voidWebSocketClient.onOpen(WebSocket conn) - -
    -          Calls subclass' implementation of onOpen.
    - voidWebSocketListener.onOpen(WebSocket conn) - -
    -          Called after onHandshakeRecieved returns true.
    - voidWebSocketServer.sendToAllExcept(WebSocket connection, - java.lang.String text) - -
    -          Sends text to all currently connected WebSocket clients, - except for the specified connection.
    -  -

    - - - - - - - - - -
    Method parameters in <Unnamed> with type arguments of type WebSocket
    - voidWebSocketServer.sendToAllExcept(java.util.Set<WebSocket> connections, - java.lang.String text) - -
    -          Sends text to all currently connected WebSocket clients, - except for those found in the Set connections.
    -  -

    -


    - - - - - - - - - - - - - - - -
    - -
    - - - -
    - - - diff --git a/doc/class-use/WebSocketClient.html b/doc/class-use/WebSocketClient.html deleted file mode 100644 index 747a79831..000000000 --- a/doc/class-use/WebSocketClient.html +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - -Uses of Class WebSocketClient - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -
    -
    -

    -Uses of Class
    WebSocketClient

    -
    -No usage of WebSocketClient -

    -


    - - - - - - - - - - - - - - - -
    - -
    - - - -
    - - - diff --git a/doc/class-use/WebSocketHandshake.html b/doc/class-use/WebSocketHandshake.html deleted file mode 100644 index 61b02fbed..000000000 --- a/doc/class-use/WebSocketHandshake.html +++ /dev/null @@ -1,187 +0,0 @@ - - - - - - -Uses of Class WebSocketHandshake - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -
    -
    -

    -Uses of Class
    WebSocketHandshake

    -
    - - - - - -
    -Uses of WebSocketHandshake in <Unnamed>
    -  -

    - - - - - - - - - - - - - - - - - -
    Methods in <Unnamed> with parameters of type WebSocketHandshake
    - booleanWebSocketServer.onHandshakeRecieved(WebSocket conn, - WebSocketHandshake handshake) - -
    -          Called by a WebSocket instance when a client connection has - finished sending a handshake.
    - booleanWebSocketClient.onHandshakeRecieved(WebSocket conn, - WebSocketHandshake handshake) - -
    -          Parses the server's handshake to verify that it's a valid WebSocket - handshake.
    - booleanWebSocketListener.onHandshakeRecieved(WebSocket conn, - WebSocketHandshake handshake) - -
    -          Called when the socket connection is first established, and the WebSocket - handshake has been recieved.
    -  -

    -


    - - - - - - - - - - - - - - - -
    - -
    - - - -
    - - - diff --git a/doc/class-use/WebSocketListener.html b/doc/class-use/WebSocketListener.html deleted file mode 100644 index 551475faa..000000000 --- a/doc/class-use/WebSocketListener.html +++ /dev/null @@ -1,191 +0,0 @@ - - - - - - -Uses of Interface WebSocketListener - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -
    -
    -

    -Uses of Interface
    WebSocketListener

    -
    - - - - - -
    -Uses of WebSocketListener in <Unnamed>
    -  -

    - - - - - - - - - - - - - -
    Classes in <Unnamed> that implement WebSocketListener
    - classWebSocketClient - -
    -          The WebSocketClient is an abstract class that expects a valid - "ws://" URI to connect to.
    - classWebSocketServer - -
    -          WebSocketServer is an abstract class that only takes care of the - HTTP handshake portion of WebSockets.
    -  -

    - - - - - - - - -
    Constructors in <Unnamed> with parameters of type WebSocketListener
    WebSocket(java.nio.channels.SocketChannel socketChannel, - WebSocketListener listener, - WebSocketProtocol.ClientServerType wstype) - -
    -          Used in WebSocketServer and WebSocketClient.
    -  -

    -


    - - - - - - - - - - - - - - - -
    - -
    - - - -
    - - - diff --git a/doc/class-use/WebSocketProtocol.ClientServerType.html b/doc/class-use/WebSocketProtocol.ClientServerType.html deleted file mode 100644 index 17e038d83..000000000 --- a/doc/class-use/WebSocketProtocol.ClientServerType.html +++ /dev/null @@ -1,222 +0,0 @@ - - - - - - -Uses of Class WebSocketProtocol.ClientServerType - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -
    -
    -

    -Uses of Class
    WebSocketProtocol.ClientServerType

    -
    - - - - - -
    -Uses of WebSocketProtocol.ClientServerType in <Unnamed>
    -  -

    - - - - - - - - - - - - - - - - - -
    Methods in <Unnamed> that return WebSocketProtocol.ClientServerType
    - WebSocketProtocol.ClientServerTypeWebSocketHandshake.getType() - -
    -           
    -static WebSocketProtocol.ClientServerTypeWebSocketProtocol.ClientServerType.valueOf(java.lang.String name) - -
    -          Returns the enum constant of this type with the specified name.
    -static WebSocketProtocol.ClientServerType[]WebSocketProtocol.ClientServerType.values() - -
    -          Returns an array containing the constants of this enum type, in -the order they are declared.
    -  -

    - - - - - - - - - -
    Methods in <Unnamed> with parameters of type WebSocketProtocol.ClientServerType
    - voidWebSocketHandshake.setType(WebSocketProtocol.ClientServerType handshakeType) - -
    -           
    -  -

    - - - - - - - - - - - -
    Constructors in <Unnamed> with parameters of type WebSocketProtocol.ClientServerType
    WebSocket(java.nio.channels.SocketChannel socketChannel, - WebSocketListener listener, - WebSocketProtocol.ClientServerType wstype) - -
    -          Used in WebSocketServer and WebSocketClient.
    WebSocketHandshake(byte[] handshake, - WebSocketProtocol.ClientServerType type, - WebSocketProtocol.Draft draft) - -
    -           
    -  -

    -


    - - - - - - - - - - - - - - - -
    - -
    - - - -
    - - - diff --git a/doc/class-use/WebSocketProtocol.Draft.html b/doc/class-use/WebSocketProtocol.Draft.html deleted file mode 100644 index e5ec00337..000000000 --- a/doc/class-use/WebSocketProtocol.Draft.html +++ /dev/null @@ -1,273 +0,0 @@ - - - - - - -Uses of Class WebSocketProtocol.Draft - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -
    -
    -

    -Uses of Class
    WebSocketProtocol.Draft

    -
    - - - - - -
    -Uses of WebSocketProtocol.Draft in <Unnamed>
    -  -

    - - - - - - - - - -
    Fields in <Unnamed> declared as WebSocketProtocol.Draft
    -static WebSocketProtocol.DraftWebSocketServer.DEFAULT_DRAFT - -
    -          If a constructor is used that doesn't define the draft, the DEFAULT_DRAFT - will be the draft of the WebSocket protocl the WebSocketServer supports.
    -  -

    - - - - - - - - - - - - - - - - - - - - - - - - - -
    Methods in <Unnamed> that return WebSocketProtocol.Draft
    - WebSocketProtocol.DraftWebSocketServer.getDraft() - -
    -          Gets the draft that this WebSocketClient supports.
    - WebSocketProtocol.DraftWebSocketClient.getDraft() - -
    -          Gets the draft that this WebSocketClient supports.
    - WebSocketProtocol.DraftWebSocketHandshake.getDraft() - -
    -           
    -static WebSocketProtocol.DraftWebSocketProtocol.Draft.valueOf(java.lang.String name) - -
    -          Returns the enum constant of this type with the specified name.
    -static WebSocketProtocol.Draft[]WebSocketProtocol.Draft.values() - -
    -          Returns an array containing the constants of this enum type, in -the order they are declared.
    -  -

    - - - - - - - - - - - - - - - - - -
    Methods in <Unnamed> with parameters of type WebSocketProtocol.Draft
    - voidWebSocketServer.setDraft(WebSocketProtocol.Draft draft) - -
    -          Sets this WebSocketClient draft.
    - voidWebSocketClient.setDraft(WebSocketProtocol.Draft draft) - -
    -          Sets this WebSocketClient draft.
    - voidWebSocketHandshake.setDraft(WebSocketProtocol.Draft handshakeDraft) - -
    -           
    -  -

    - - - - - - - - - - - -
    Constructors in <Unnamed> with parameters of type WebSocketProtocol.Draft
    WebSocketHandshake(byte[] handshake, - WebSocketProtocol.ClientServerType type, - WebSocketProtocol.Draft draft) - -
    -           
    WebSocketServer(int port, - java.lang.String origin, - java.lang.String subprotocol, - WebSocketProtocol.Draft draft) - -
    -          Creates a WebSocketServer that will attempt to listen on port - port using a specified subprotocol.
    -  -

    -


    - - - - - - - - - - - - - - - -
    - -
    - - - -
    - - - diff --git a/doc/class-use/WebSocketProtocol.html b/doc/class-use/WebSocketProtocol.html deleted file mode 100644 index 6cfd50d34..000000000 --- a/doc/class-use/WebSocketProtocol.html +++ /dev/null @@ -1,207 +0,0 @@ - - - - - - -Uses of Interface WebSocketProtocol - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -
    -
    -

    -Uses of Interface
    WebSocketProtocol

    -
    - - - - - -
    -Uses of WebSocketProtocol in <Unnamed>
    -  -

    - - - - - - - - - -
    Subinterfaces of WebSocketProtocol in <Unnamed>
    -(package private)  interfaceWebSocketListener - -
    -          Implemented by WebSocketClient and WebSocketServer.
    -  -

    - - - - - - - - - - - - - - - - - - - - - -
    Classes in <Unnamed> that implement WebSocketProtocol
    - classWebSocket - -
    -          Represents one end (client or server) of a single WebSocket connection.
    - classWebSocketClient - -
    -          The WebSocketClient is an abstract class that expects a valid - "ws://" URI to connect to.
    - classWebSocketHandshake - -
    -          The WebSocketHandshake is a class used to create and read client and server handshakes.
    - classWebSocketServer - -
    -          WebSocketServer is an abstract class that only takes care of the - HTTP handshake portion of WebSockets.
    -  -

    -


    - - - - - - - - - - - - - - - -
    - -
    - - - -
    - - - diff --git a/doc/class-use/WebSocketServer.html b/doc/class-use/WebSocketServer.html deleted file mode 100644 index 708f86111..000000000 --- a/doc/class-use/WebSocketServer.html +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - -Uses of Class WebSocketServer - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -
    -
    -

    -Uses of Class
    WebSocketServer

    -
    -No usage of WebSocketServer -

    -


    - - - - - - - - - - - - - - - -
    - -
    - - - -
    - - - diff --git a/doc/constant-values.html b/doc/constant-values.html index b36780f0b..05bedad83 100644 --- a/doc/constant-values.html +++ b/doc/constant-values.html @@ -2,12 +2,12 @@ - + Constant Field Values - + @@ -37,12 +37,11 @@ - + - - + - +
    @@ -84,14 +83,14 @@


    Contents - + +net.tootallnate.*
    -<Unnamed>.*
    @@ -99,30 +98,30 @@

    - + - - + - - + - - + - - + @@ -135,18 +134,18 @@

    WebSocketnet.tootallnate.websocket.WebSocket
    + public static final byteCRCR 13
    + public static final byteEND_OF_FRAMEEND_OF_FRAME -1
    + public static final byteLFLF 10
    + public static final byteSTART_OF_FRAMESTART_OF_FRAME 0
    - + - - + - - + @@ -168,12 +167,11 @@

    WebSocketServernet.tootallnate.websocket.WebSocketServer
    + public static final intDEFAULT_PORTDEFAULT_PORT 80
    + public static final java.lang.StringFLASH_POLICY_REQUESTFLASH_POLICY_REQUEST "<policy-file-request/>\u0000"
    - + - - + - +
    diff --git a/doc/deprecated-list.html b/doc/deprecated-list.html index 7200416a0..7ac9007e0 100644 --- a/doc/deprecated-list.html +++ b/doc/deprecated-list.html @@ -2,12 +2,12 @@ - + Deprecated List - + @@ -37,12 +37,11 @@ - + - - + - +
    @@ -98,12 +97,11 @@

    - + - - + - +
    diff --git a/doc/help-doc.html b/doc/help-doc.html index 4f1502c6b..44b727f23 100644 --- a/doc/help-doc.html +++ b/doc/help-doc.html @@ -2,12 +2,12 @@ - + API Help - + @@ -37,12 +37,11 @@ - + - - + - +
    @@ -121,10 +120,6 @@

  • Enum declaration
  • Enum description
  • Enum Constant Summary
  • Enum Constant Detail

    -Use

    -
    -Each documented package, class and interface has its own Use page. This page describes what packages, classes, methods, constructors and fields use any part of the given class or package. Given a class or interface A, its Use page includes subclasses of A, fields declared as A, methods that return A, and methods and constructors with parameters of type A. You can access this page by first going to the package, class or interface, then clicking on the "Use" link in the navigation bar.
    -

    Tree (Class Hierarchy)

    There is a Class Hierarchy page for all packages, plus a hierarchy for each package. Each hierarchy page contains a list of classes and a list of interfaces. The classes are organized by inheritance structure starting with java.lang.Object. The interfaces do not inherit from java.lang.Object.
      @@ -137,7 +132,7 @@

      Index

      -The Index contains an alphabetic list of all classes, interfaces, constructors, methods, and fields.
      +The Index contains an alphabetic list of all classes, interfaces, constructors, methods, and fields.

    Prev/Next

    These links take you to the next or previous class, interface, package, or related page.

    @@ -169,12 +164,11 @@

    - + - - + - +
    diff --git a/doc/index-all.html b/doc/index-all.html new file mode 100644 index 000000000..8db5d286b --- /dev/null +++ b/doc/index-all.html @@ -0,0 +1,471 @@ + + + + + + +Index + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +B C D E F G L N O P R S T W
    +

    +B

    +
    +
    buildHandshake() - +Method in class net.tootallnate.websocket.WebSocketHandshake +
    Generates the handshake byte array based on the handshake draft + and type as well as the keys set for this handshake. +
    +
    +

    +C

    +
    +
    close() - +Method in class net.tootallnate.websocket.WebSocket +
    Closes the underlying SocketChannel, and calls the listener's onClose event + handler. +
    close() - +Method in class net.tootallnate.websocket.WebSocketClient +
    Calls close on the underlying SocketChannel, which in turn + closes the socket connection, and ends the client socket thread. +
    connect() - +Method in class net.tootallnate.websocket.WebSocketClient +
    Starts a background thread that attempts and maintains a WebSocket + connection to the URI specified in the constructor or via + setURI. +
    connections() - +Method in class net.tootallnate.websocket.WebSocketServer +
    Returns a WebSocket[] of currently connected clients. +
    CR - +Static variable in class net.tootallnate.websocket.WebSocket +
    The byte representing CR, or Carriage Return, or \r +
    +
    +

    +D

    +
    +
    DEFAULT_DRAFT - +Static variable in class net.tootallnate.websocket.WebSocketServer +
    If a constructor is used that doesn't define the draft, the DEFAULT_DRAFT + will be the draft of the WebSocket protocl the WebSocketServer supports. +
    DEFAULT_PORT - +Static variable in class net.tootallnate.websocket.WebSocketServer +
    If the nullary constructor is used, the DEFAULT_PORT will be the port the + WebSocketServer is binded to. +
    +
    +

    +E

    +
    +
    END_OF_FRAME - +Static variable in class net.tootallnate.websocket.WebSocket +
    The byte representing the end of a WebSocket text frame. +
    +
    +

    +F

    +
    +
    FLASH_POLICY_REQUEST - +Static variable in class net.tootallnate.websocket.WebSocketServer +
    The value of handshake when a Flash client requests a policy + file on this server. +
    +
    +

    +G

    +
    +
    getAsByteArray(String) - +Method in class net.tootallnate.websocket.WebSocketHandshake +
      +
    getDraft() - +Method in class net.tootallnate.websocket.WebSocketClient +
    Gets the draft that this WebSocketClient supports. +
    getDraft() - +Method in class net.tootallnate.websocket.WebSocketHandshake +
      +
    getDraft() - +Method in class net.tootallnate.websocket.WebSocketServer +
    Gets the draft that this WebSocketClient supports. +
    getFlashSecurityPolicy() - +Method in class net.tootallnate.websocket.WebSocketServer +
    Gets the XML string that should be returned if a client requests a Flash + security policy. +
    getHandshake() - +Method in class net.tootallnate.websocket.WebSocketHandshake +
      +
    getOrigin() - +Method in class net.tootallnate.websocket.WebSocketServer +
    Gets the origin that this WebSocketServer should allow connections from. +
    getPort() - +Method in class net.tootallnate.websocket.WebSocketServer +
    Gets the port number that this server listens on. +
    getProperty(String) - +Method in class net.tootallnate.websocket.WebSocketHandshake +
      +
    getSubProtocol() - +Method in class net.tootallnate.websocket.WebSocketClient +
    Gets the subprotocol that this WebSocketClient supports. +
    getSubProtocol() - +Method in class net.tootallnate.websocket.WebSocketServer +
    Gets the subprotocol that this WebSocketClient supports. +
    getType() - +Method in class net.tootallnate.websocket.WebSocketHandshake +
      +
    getURI() - +Method in class net.tootallnate.websocket.WebSocketClient +
    Gets the URI that this WebSocketClient is connected to (or should attempt + to connect to). +
    +
    +

    +L

    +
    +
    LF - +Static variable in class net.tootallnate.websocket.WebSocket +
    The byte representing LF, or Line Feed, or \n +
    +
    +

    +N

    +
    +
    net.tootallnate.websocket - package net.tootallnate.websocket
     
    newBufferQueue() - +Method in class net.tootallnate.websocket.WebSocketServer +
      +
    +
    +

    +O

    +
    +
    onClientClose(WebSocket) - +Method in class net.tootallnate.websocket.WebSocketServer +
      +
    onClientMessage(WebSocket, String) - +Method in class net.tootallnate.websocket.WebSocketServer +
      +
    onClientOpen(WebSocket) - +Method in class net.tootallnate.websocket.WebSocketServer +
      +
    onClose(WebSocket) - +Method in class net.tootallnate.websocket.WebSocketClient +
    Calls subclass' implementation of onClose. +
    onClose() - +Method in class net.tootallnate.websocket.WebSocketClient +
      +
    onClose(WebSocket) - +Method in class net.tootallnate.websocket.WebSocketServer +
      +
    onHandshakeRecieved(WebSocket, WebSocketHandshake) - +Method in class net.tootallnate.websocket.WebSocketClient +
    Parses the server's handshake to verify that it's a valid WebSocket + handshake. +
    onHandshakeRecieved(WebSocket, WebSocketHandshake) - +Method in class net.tootallnate.websocket.WebSocketServer +
    Called by a WebSocket instance when a client connection has + finished sending a handshake. +
    onMessage(WebSocket, String) - +Method in class net.tootallnate.websocket.WebSocketClient +
    Calls subclass' implementation of onMessage. +
    onMessage(String) - +Method in class net.tootallnate.websocket.WebSocketClient +
      +
    onMessage(WebSocket, String) - +Method in class net.tootallnate.websocket.WebSocketServer +
      +
    onOpen(WebSocket) - +Method in class net.tootallnate.websocket.WebSocketClient +
    Calls subclass' implementation of onOpen. +
    onOpen() - +Method in class net.tootallnate.websocket.WebSocketClient +
      +
    onOpen(WebSocket) - +Method in class net.tootallnate.websocket.WebSocketServer +
      +
    +
    +

    +P

    +
    +
    parseHandshake() - +Method in class net.tootallnate.websocket.WebSocketHandshake +
    Parses the parts of the handshake into Hashtable keys. +
    +
    +

    +R

    +
    +
    run() - +Method in class net.tootallnate.websocket.WebSocketClient +
      +
    run() - +Method in class net.tootallnate.websocket.WebSocketServer +
      +
    +
    +

    +S

    +
    +
    send(String) - +Method in class net.tootallnate.websocket.WebSocket +
      +
    send(String) - +Method in class net.tootallnate.websocket.WebSocketClient +
    Sends text to the connected WebSocket server. +
    sendTo(WebSocket, String) - +Method in class net.tootallnate.websocket.WebSocketServer +
    Sends text to connected WebSocket client specified by + connection. +
    sendTo(Set<WebSocket>, String) - +Method in class net.tootallnate.websocket.WebSocketServer +
    Sends text to all currently connected WebSocket clients found in + the Set connections. +
    sendToAll(String) - +Method in class net.tootallnate.websocket.WebSocketServer +
    Sends text to all currently connected WebSocket clients. +
    sendToAllExcept(WebSocket, String) - +Method in class net.tootallnate.websocket.WebSocketServer +
    Sends text to all currently connected WebSocket clients, except + for the specified connection. +
    sendToAllExcept(Set<WebSocket>, String) - +Method in class net.tootallnate.websocket.WebSocketServer +
    Sends text to all currently connected WebSocket clients, except + for those found in the Set connections. +
    setDraft(WebSocketProtocol.Draft) - +Method in class net.tootallnate.websocket.WebSocketClient +
    Sets this WebSocketClient draft. +
    setDraft(WebSocketProtocol.Draft) - +Method in class net.tootallnate.websocket.WebSocketHandshake +
      +
    setDraft(WebSocketProtocol.Draft) - +Method in class net.tootallnate.websocket.WebSocketServer +
    Sets this WebSocketClient draft. +
    setHandshake(byte[]) - +Method in class net.tootallnate.websocket.WebSocketHandshake +
      +
    setOrigin(String) - +Method in class net.tootallnate.websocket.WebSocketServer +
    Sets the origin that this WebSocketServer should allow connections from. +
    setPort(int) - +Method in class net.tootallnate.websocket.WebSocketServer +
    Sets the port that this WebSocketServer should listen on. +
    setSubProtocol(String) - +Method in class net.tootallnate.websocket.WebSocketClient +
    Sets this WebSocketClient subprotocol. +
    setSubProtocol(String) - +Method in class net.tootallnate.websocket.WebSocketServer +
    Sets this WebSocketClient subprotocol. +
    setType(WebSocketProtocol.ClientServerType) - +Method in class net.tootallnate.websocket.WebSocketHandshake +
      +
    setURI(URI) - +Method in class net.tootallnate.websocket.WebSocketClient +
    Sets this WebSocketClient to connect to the specified URI. +
    socketChannel() - +Method in class net.tootallnate.websocket.WebSocket +
      +
    start() - +Method in class net.tootallnate.websocket.WebSocketServer +
    Starts the server thread that binds to the currently set port number and + listeners for WebSocket connection requests. +
    START_OF_FRAME - +Static variable in class net.tootallnate.websocket.WebSocket +
    The byte representing the beginning of a WebSocket text frame. +
    stop() - +Method in class net.tootallnate.websocket.WebSocketServer +
    Closes all connected clients sockets, then closes the underlying + ServerSocketChannel, effectively killing the server socket thread and + freeing the port the server was bound to. +
    +
    +

    +T

    +
    +
    toString() - +Method in class net.tootallnate.websocket.WebSocketHandshake +
      +
    +
    +

    +W

    +
    +
    WebSocket - Class in net.tootallnate.websocket
    Represents one end (client or server) of a single WebSocket connection.
    WebSocket(SocketChannel, BlockingQueue<ByteBuffer>, WebSocketListener, WebSocketProtocol.ClientServerType) - +Constructor for class net.tootallnate.websocket.WebSocket +
    Used in WebSocketServer and WebSocketClient. +
    WebSocketClient - Class in net.tootallnate.websocket
    The WebSocketClient is an abstract class that expects a valid + "ws://" URI to connect to.
    WebSocketClient() - +Constructor for class net.tootallnate.websocket.WebSocketClient +
    Nullary constructor. +
    WebSocketClient(URI) - +Constructor for class net.tootallnate.websocket.WebSocketClient +
    Constructs a WebSocketClient instance and sets it to the connect to the + specified URI. +
    WebSocketClient(URI, WebSocketProtocol.Draft) - +Constructor for class net.tootallnate.websocket.WebSocketClient +
    Constructs a WebSocketClient instance and sets it to the connect to the + specified URI. +
    WebSocketClient(URI, String) - +Constructor for class net.tootallnate.websocket.WebSocketClient +
    Constructs a WebSocketClient instance and sets it to the connect to the + specified URI using the specified subprotocol and draft. +
    WebSocketClient(URI, String, String) - +Constructor for class net.tootallnate.websocket.WebSocketClient +
    Constructs a WebSocketClient instance and sets it to the connect to the + specified URI using the specified subprotocol and draft. +
    WebSocketHandshake - Class in net.tootallnate.websocket
    The WebSocketHandshake is a class used to create and read client and + server handshakes.
    WebSocketHandshake() - +Constructor for class net.tootallnate.websocket.WebSocketHandshake +
      +
    WebSocketHandshake(byte[]) - +Constructor for class net.tootallnate.websocket.WebSocketHandshake +
      +
    WebSocketHandshake(byte[], WebSocketProtocol.ClientServerType, WebSocketProtocol.Draft) - +Constructor for class net.tootallnate.websocket.WebSocketHandshake +
      +
    WebSocketServer - Class in net.tootallnate.websocket
    WebSocketServer is an abstract class that only takes care of the + HTTP handshake portion of WebSockets.
    WebSocketServer() - +Constructor for class net.tootallnate.websocket.WebSocketServer +
    Nullary constructor. +
    WebSocketServer(int) - +Constructor for class net.tootallnate.websocket.WebSocketServer +
    Creates a WebSocketServer that will attempt to listen on port + port. +
    WebSocketServer(int, String) - +Constructor for class net.tootallnate.websocket.WebSocketServer +
    Creates a WebSocketServer that will attempt to listen on port + port. +
    WebSocketServer(int, String, String) - +Constructor for class net.tootallnate.websocket.WebSocketServer +
    Creates a WebSocketServer that will attempt to listen on port + port using a specified subprotocol. +
    WebSocketServer(int, String, String, String) - +Constructor for class net.tootallnate.websocket.WebSocketServer +
    Creates a WebSocketServer that will attempt to listen on port + port using a specified subprotocol. +
    WebSocketServer(int, String, String, WebSocketProtocol.Draft) - +Constructor for class net.tootallnate.websocket.WebSocketServer +
    Creates a WebSocketServer that will attempt to listen on port + port using a specified subprotocol. +
    +
    +B C D E F G L N O P R S T W + + + + + + + + + + + + + + +
    + +
    + + + +
    + + + diff --git a/doc/index-files/index-1.html b/doc/index-files/index-1.html deleted file mode 100644 index a3a1de44a..000000000 --- a/doc/index-files/index-1.html +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - -B-Index - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    -

    -B

    -
    -
    buildHandshake() - -Method in class WebSocketHandshake -
    Generates the handshake byte array based on the handshake draft and type as well as the keys - set for this handshake. -
    -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    - - - diff --git a/doc/index-files/index-10.html b/doc/index-files/index-10.html deleted file mode 100644 index c1816543f..000000000 --- a/doc/index-files/index-10.html +++ /dev/null @@ -1,198 +0,0 @@ - - - - - - -O-Index - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    -

    -O

    -
    -
    onClientClose(WebSocket) - -Method in class WebSocketServer -
      -
    onClientMessage(WebSocket, String) - -Method in class WebSocketServer -
      -
    onClientOpen(WebSocket) - -Method in class WebSocketServer -
      -
    onClose(WebSocket) - -Method in class WebSocketClient -
    Calls subclass' implementation of onClose. -
    onClose() - -Method in class WebSocketClient -
      -
    onClose(WebSocket) - -Method in interface WebSocketListener -
    Called after WebSocket#close is explicity called, or when the - other end of the WebSocket connection is closed. -
    onClose(WebSocket) - -Method in class WebSocketServer -
      -
    onHandshakeRecieved(WebSocket, WebSocketHandshake) - -Method in class WebSocketClient -
    Parses the server's handshake to verify that it's a valid WebSocket - handshake. -
    onHandshakeRecieved(WebSocket, WebSocketHandshake) - -Method in interface WebSocketListener -
    Called when the socket connection is first established, and the WebSocket - handshake has been recieved. -
    onHandshakeRecieved(WebSocket, WebSocketHandshake) - -Method in class WebSocketServer -
    Called by a WebSocket instance when a client connection has - finished sending a handshake. -
    onMessage(WebSocket, String) - -Method in class WebSocketClient -
    Calls subclass' implementation of onMessage. -
    onMessage(String) - -Method in class WebSocketClient -
      -
    onMessage(WebSocket, String) - -Method in interface WebSocketListener -
    Called when an entire text frame has been recieved. -
    onMessage(WebSocket, String) - -Method in class WebSocketServer -
      -
    onOpen(WebSocket) - -Method in class WebSocketClient -
    Calls subclass' implementation of onOpen. -
    onOpen() - -Method in class WebSocketClient -
      -
    onOpen(WebSocket) - -Method in interface WebSocketListener -
    Called after onHandshakeRecieved returns true. -
    onOpen(WebSocket) - -Method in class WebSocketServer -
      -
    -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    - - - diff --git a/doc/index-files/index-11.html b/doc/index-files/index-11.html deleted file mode 100644 index d9b71f6ef..000000000 --- a/doc/index-files/index-11.html +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - -P-Index - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    -

    -P

    -
    -
    parseHandshake() - -Method in class WebSocketHandshake -
    Parses the parts of the handshake into Hashtable keys. -
    -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    - - - diff --git a/doc/index-files/index-12.html b/doc/index-files/index-12.html deleted file mode 100644 index 3ed8771a0..000000000 --- a/doc/index-files/index-12.html +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - -R-Index - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    -

    -R

    -
    -
    run() - -Method in class WebSocketClient -
      -
    run() - -Method in class WebSocketServer -
      -
    -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    - - - diff --git a/doc/index-files/index-13.html b/doc/index-files/index-13.html deleted file mode 100644 index 22320ee6a..000000000 --- a/doc/index-files/index-13.html +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - -S-Index - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    -

    -S

    -
    -
    send(String) - -Method in class WebSocket -
      -
    send(String) - -Method in class WebSocketClient -
    Sends text to the connected WebSocket server. -
    sendToAll(String) - -Method in class WebSocketServer -
    Sends text to all currently connected WebSocket clients. -
    sendToAllExcept(WebSocket, String) - -Method in class WebSocketServer -
    Sends text to all currently connected WebSocket clients, - except for the specified connection. -
    sendToAllExcept(Set<WebSocket>, String) - -Method in class WebSocketServer -
    Sends text to all currently connected WebSocket clients, - except for those found in the Set connections. -
    setDraft(WebSocketProtocol.Draft) - -Method in class WebSocketClient -
    Sets this WebSocketClient draft. -
    setDraft(WebSocketProtocol.Draft) - -Method in class WebSocketHandshake -
      -
    setDraft(WebSocketProtocol.Draft) - -Method in class WebSocketServer -
    Sets this WebSocketClient draft. -
    setHandshake(byte[]) - -Method in class WebSocketHandshake -
      -
    setOrigin(String) - -Method in class WebSocketServer -
    Sets the origin that this WebSocketServer should allow connections - from. -
    setPort(int) - -Method in class WebSocketServer -
    Sets the port that this WebSocketServer should listen on. -
    setSubProtocol(String) - -Method in class WebSocketClient -
    Sets this WebSocketClient subprotocol. -
    setSubProtocol(String) - -Method in class WebSocketServer -
    Sets this WebSocketClient subprotocol. -
    setType(WebSocketProtocol.ClientServerType) - -Method in class WebSocketHandshake -
      -
    setURI(URI) - -Method in class WebSocketClient -
    Sets this WebSocketClient to connect to the specified URI. -
    socketChannel() - -Method in class WebSocket -
      -
    start() - -Method in class WebSocketServer -
    Starts the server thread that binds to the currently set port number and - listeners for WebSocket connection requests. -
    START_OF_FRAME - -Static variable in class WebSocket -
    The byte representing the beginning of a WebSocket text frame. -
    stop() - -Method in class WebSocketServer -
    Closes all connected clients sockets, then closes the underlying - ServerSocketChannel, effectively killing the server socket thread and - freeing the port the server was bound to. -
    -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    - - - diff --git a/doc/index-files/index-14.html b/doc/index-files/index-14.html deleted file mode 100644 index 13856c032..000000000 --- a/doc/index-files/index-14.html +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - -T-Index - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    -

    -T

    -
    -
    toString() - -Method in class WebSocketHandshake -
      -
    -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    - - - diff --git a/doc/index-files/index-15.html b/doc/index-files/index-15.html deleted file mode 100644 index d893e9499..000000000 --- a/doc/index-files/index-15.html +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - -U-Index - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    -

    -U

    -
    -
    UTF8_CHARSET - -Static variable in interface WebSocketProtocol -
    The WebSocket protocol expects UTF-8 encoded bytes. -
    -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    - - - diff --git a/doc/index-files/index-16.html b/doc/index-files/index-16.html deleted file mode 100644 index 6fbd829da..000000000 --- a/doc/index-files/index-16.html +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - -V-Index - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    -

    -V

    -
    -
    valueOf(String) - -Static method in enum WebSocketProtocol.ClientServerType -
    Returns the enum constant of this type with the specified name. -
    valueOf(String) - -Static method in enum WebSocketProtocol.Draft -
    Returns the enum constant of this type with the specified name. -
    values() - -Static method in enum WebSocketProtocol.ClientServerType -
    Returns an array containing the constants of this enum type, in -the order they are declared. -
    values() - -Static method in enum WebSocketProtocol.Draft -
    Returns an array containing the constants of this enum type, in -the order they are declared. -
    -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    - - - diff --git a/doc/index-files/index-17.html b/doc/index-files/index-17.html deleted file mode 100644 index 4b05322f8..000000000 --- a/doc/index-files/index-17.html +++ /dev/null @@ -1,192 +0,0 @@ - - - - - - -W-Index - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    -

    -W

    -
    -
    WebSocket - Class in <Unnamed>
    Represents one end (client or server) of a single WebSocket connection.
    WebSocket(SocketChannel, WebSocketListener, WebSocketProtocol.ClientServerType) - -Constructor for class WebSocket -
    Used in WebSocketServer and WebSocketClient. -
    WebSocketClient - Class in <Unnamed>
    The WebSocketClient is an abstract class that expects a valid - "ws://" URI to connect to.
    WebSocketClient() - -Constructor for class WebSocketClient -
    Nullary constructor. -
    WebSocketClient(URI) - -Constructor for class WebSocketClient -
    Constructs a WebSocketClient instance and sets it to the connect to the - specified URI. -
    WebSocketClient(URI, String) - -Constructor for class WebSocketClient -
    Constructs a WebSocketClient instance and sets it to the connect to the - specified URI using the specified subprotocol. -
    WebSocketClient(URI, String, String) - -Constructor for class WebSocketClient -
    Constructs a WebSocketClient instance and sets it to the connect to the - specified URI using the specified subprotocol and draft. -
    WebSocketHandshake - Class in <Unnamed>
    The WebSocketHandshake is a class used to create and read client and server handshakes.
    WebSocketHandshake() - -Constructor for class WebSocketHandshake -
      -
    WebSocketHandshake(byte[]) - -Constructor for class WebSocketHandshake -
      -
    WebSocketHandshake(byte[], WebSocketProtocol.ClientServerType, WebSocketProtocol.Draft) - -Constructor for class WebSocketHandshake -
      -
    WebSocketListener - Interface in <Unnamed>
    Implemented by WebSocketClient and WebSocketServer.
    WebSocketProtocol - Interface in <Unnamed>
    Constants used by WebSocket protocol.
    WebSocketProtocol.ClientServerType - Enum in <Unnamed>
    The type of WebSocket
    WebSocketProtocol.Draft - Enum in <Unnamed>
    The version of the WebSocket Internet-Draft
    WebSocketServer - Class in <Unnamed>
    WebSocketServer is an abstract class that only takes care of the - HTTP handshake portion of WebSockets.
    WebSocketServer() - -Constructor for class WebSocketServer -
    Nullary constructor. -
    WebSocketServer(int) - -Constructor for class WebSocketServer -
    Creates a WebSocketServer that will attempt to listen on port - port. -
    WebSocketServer(int, String) - -Constructor for class WebSocketServer -
    Creates a WebSocketServer that will attempt to listen on port - port. -
    WebSocketServer(int, String, String) - -Constructor for class WebSocketServer -
    Creates a WebSocketServer that will attempt to listen on port - port using a specified subprotocol. -
    WebSocketServer(int, String, String, String) - -Constructor for class WebSocketServer -
    Creates a WebSocketServer that will attempt to listen on port - port using a specified subprotocol. -
    WebSocketServer(int, String, String, WebSocketProtocol.Draft) - -Constructor for class WebSocketServer -
    Creates a WebSocketServer that will attempt to listen on port - port using a specified subprotocol. -
    -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    - - - diff --git a/doc/index-files/index-2.html b/doc/index-files/index-2.html deleted file mode 100644 index d128c3eea..000000000 --- a/doc/index-files/index-2.html +++ /dev/null @@ -1,158 +0,0 @@ - - - - - - -C-Index - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    -

    -C

    -
    -
    close() - -Method in class WebSocket -
    Closes the underlying SocketChannel, and calls the listener's onClose - event handler. -
    close() - -Method in class WebSocketClient -
    Calls close on the underlying SocketChannel, which in turn - closes the socket connection, and ends the client socket thread. -
    connect() - -Method in class WebSocketClient -
    Starts a background thread that attempts and maintains a WebSocket - connection to the URI specified in the constructor or via setURI. -
    connections() - -Method in class WebSocketServer -
    Returns a WebSocket[] of currently connected clients. -
    CR - -Static variable in class WebSocket -
    The byte representing CR, or Carriage Return, or \r -
    -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    - - - diff --git a/doc/index-files/index-3.html b/doc/index-files/index-3.html deleted file mode 100644 index 0e08576f3..000000000 --- a/doc/index-files/index-3.html +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - -D-Index - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    -

    -D

    -
    -
    DEFAULT_DRAFT - -Static variable in class WebSocketServer -
    If a constructor is used that doesn't define the draft, the DEFAULT_DRAFT - will be the draft of the WebSocket protocl the WebSocketServer supports. -
    DEFAULT_PORT - -Static variable in class WebSocketServer -
    If the nullary constructor is used, the DEFAULT_PORT will be the port - the WebSocketServer is binded to. -
    -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    - - - diff --git a/doc/index-files/index-4.html b/doc/index-files/index-4.html deleted file mode 100644 index e13437a28..000000000 --- a/doc/index-files/index-4.html +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - -E-Index - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    -

    -E

    -
    -
    END_OF_FRAME - -Static variable in class WebSocket -
    The byte representing the end of a WebSocket text frame. -
    -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    - - - diff --git a/doc/index-files/index-5.html b/doc/index-files/index-5.html deleted file mode 100644 index bf8bbae66..000000000 --- a/doc/index-files/index-5.html +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - -F-Index - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    -

    -F

    -
    -
    FLASH_POLICY_REQUEST - -Static variable in class WebSocketServer -
    The value of handshake when a Flash client requests a policy - file on this server. -
    -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    - - - diff --git a/doc/index-files/index-6.html b/doc/index-files/index-6.html deleted file mode 100644 index 301b13864..000000000 --- a/doc/index-files/index-6.html +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - -G-Index - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    -

    -G

    -
    -
    getAsByteArray(String) - -Method in class WebSocketHandshake -
      -
    getDraft() - -Method in class WebSocketClient -
    Gets the draft that this WebSocketClient supports. -
    getDraft() - -Method in class WebSocketHandshake -
      -
    getDraft() - -Method in class WebSocketServer -
    Gets the draft that this WebSocketClient supports. -
    getFlashSecurityPolicy() - -Method in class WebSocketServer -
    Gets the XML string that should be returned if a client requests a Flash - security policy. -
    getHandshake() - -Method in class WebSocketHandshake -
      -
    getOrigin() - -Method in class WebSocketServer -
    Gets the origin that this WebSocketServer should allow connections - from. -
    getPort() - -Method in class WebSocketServer -
    Gets the port number that this server listens on. -
    getProperty(String) - -Method in class WebSocketHandshake -
      -
    getSubProtocol() - -Method in class WebSocketClient -
    Gets the subprotocol that this WebSocketClient supports. -
    getSubProtocol() - -Method in class WebSocketServer -
    Gets the subprotocol that this WebSocketClient supports. -
    getType() - -Method in class WebSocketHandshake -
      -
    getURI() - -Method in class WebSocketClient -
    Gets the URI that this WebSocketClient is connected to (or should attempt - to connect to). -
    -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    - - - diff --git a/doc/index-files/index-7.html b/doc/index-files/index-7.html deleted file mode 100644 index 6c6b8c124..000000000 --- a/doc/index-files/index-7.html +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - -H-Index - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    -

    -H

    -
    -
    handleRead() - -Method in class WebSocket -
    Should be called when a Selector has a key that is writable for this - WebSocket's SocketChannel connection. -
    -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    - - - diff --git a/doc/index-files/index-8.html b/doc/index-files/index-8.html deleted file mode 100644 index 0592ab2fe..000000000 --- a/doc/index-files/index-8.html +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - -L-Index - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    -

    -L

    -
    -
    LF - -Static variable in class WebSocket -
    The byte representing LF, or Line Feed, or \n -
    -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    - - - diff --git a/doc/index-files/index-9.html b/doc/index-files/index-9.html deleted file mode 100644 index 8df3f9d7b..000000000 --- a/doc/index-files/index-9.html +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - -M-Index - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    -

    -M

    -
    -
    MAX_KEY_VALUE - -Static variable in interface WebSocketProtocol -
    The maximum value of a WebSocket key. -
    -
    - - - - - - - - - - - - - - - -
    - -
    - - - -B C D E F G H L M O P R S T U V W
    - - - diff --git a/doc/index.html b/doc/index.html index 22c564e35..b270693da 100644 --- a/doc/index.html +++ b/doc/index.html @@ -2,7 +2,7 @@ - + Generated Documentation (Untitled) @@ -22,7 +22,7 @@ - + <H2> Frame Alert</H2> @@ -30,7 +30,7 @@ <H2> <P> This document is designed to be viewed using the frames feature. If you see this message, you are using a non-frame-capable web client. <BR> -Link to<A HREF="WebSocket.html">Non-frame version.</A> +Link to<A HREF="net/tootallnate/websocket/package-summary.html">Non-frame version.</A> diff --git a/doc/WebSocket.html b/doc/net/tootallnate/websocket/WebSocket.html similarity index 53% rename from doc/WebSocket.html rename to doc/net/tootallnate/websocket/WebSocket.html index 15fe3b7e0..e990a1617 100644 --- a/doc/WebSocket.html +++ b/doc/net/tootallnate/websocket/WebSocket.html @@ -2,14 +2,14 @@ - + WebSocket - + - + @@ -75,7 +74,7 @@ - SUMMARY: NESTED | FIELD | CONSTR | METHOD + SUMMARY: NESTED | FIELD | CONSTR | METHOD DETAIL: FIELD | CONSTR | METHOD @@ -86,24 +85,24 @@

    + +net.tootallnate.websocket +
    Class WebSocket

     java.lang.Object
    -  extended by WebSocket
    +  extended by net.tootallnate.websocket.WebSocket
     
    -
    -
    All Implemented Interfaces:
    WebSocketProtocol
    -

    -
    public final class WebSocket
    extends java.lang.Object
    implements WebSocketProtocol
    +
    public final class WebSocket
    extends java.lang.Object
  • -Represents one end (client or server) of a single WebSocket connection. - Takes care of the "handshake" phase, then allows for easy sending of - text frames, and recieving frames through an event-based model. - +Represents one end (client or server) of a single WebSocket connection. Takes + care of the "handshake" phase, then allows for easy sending of text frames, + and recieving frames through an event-based model. + This is an inner class, used by WebSocketClient and WebSocketServer, and should never need to be instantiated directly by your code. @@ -125,18 +124,24 @@

    Nested Class Summary - -  - - - + + + - + +
    Nested classes/interfaces inherited from interface WebSocketProtocol
    +static classWebSocketProtocol.ClientServerType + +
    +          The type of WebSocket
    WebSocketProtocol.ClientServerType, WebSocketProtocol.Draft +static classWebSocketProtocol.Draft + +
    +          The version of the WebSocket Internet-Draft
    -  - @@ -147,7 +152,7 @@

    - @@ -155,7 +160,7 @@

    - @@ -163,27 +168,34 @@

    - + + + + - -
    static byteCR +CR
              The byte representing CR, or Carriage Return, or \r
    static byteEND_OF_FRAME +END_OF_FRAME
              The byte representing the end of a WebSocket text frame.
    static byteLF +LF
              The byte representing LF, or Line Feed, or \n
    +static java.lang.LongMAX_KEY_VALUE + +
    +          The maximum value of a WebSocket key.
    static byteSTART_OF_FRAME +START_OF_FRAME
              The byte representing the beginning of a WebSocket text frame.
    - - - - - + +
    Fields inherited from interface WebSocketProtocol
    MAX_KEY_VALUE, UTF8_CHARSET +static java.nio.charset.CharsetUTF8_CHARSET + +
    +          The WebSocket protocol expects UTF-8 encoded bytes.
      @@ -196,12 +208,13 @@

    Constructor Summary -WebSocket(java.nio.channels.SocketChannel socketChannel, - WebSocketListener listener, - WebSocketProtocol.ClientServerType wstype) +WebSocket(java.nio.channels.SocketChannel socketChannel, + java.util.concurrent.BlockingQueue<java.nio.ByteBuffer> bufferQueue, + net.tootallnate.websocket.WebSocketListener listener, + WebSocketProtocol.ClientServerType wstype)
    -          Used in WebSocketServer and WebSocketClient. +          Used in WebSocketServer and WebSocketClient.   @@ -216,25 +229,16 @@

     void -close() - -
    -          Closes the underlying SocketChannel, and calls the listener's onClose - event handler. - - - - void -handleRead() +close()
    -          Should be called when a Selector has a key that is writable for this - WebSocket's SocketChannel connection. +          Closes the underlying SocketChannel, and calls the listener's onClose event + handler. - void -send(java.lang.String text) + boolean +send(java.lang.String text)
                @@ -242,7 +246,7 @@

     java.nio.channels.SocketChannel -socketChannel() +socketChannel()
                @@ -278,7 +282,7 @@

    The byte representing CR, or Carriage Return, or \r

    -
    See Also:
    Constant Field Values
    +
    See Also:
    Constant Field Values
    @@ -290,7 +294,7 @@

    The byte representing LF, or Line Feed, or \n

    -
    See Also:
    Constant Field Values
    +
    See Also:
    Constant Field Values
    @@ -302,7 +306,7 @@

    The byte representing the beginning of a WebSocket text frame.

    -
    See Also:
    Constant Field Values
    +
    See Also:
    Constant Field Values
    @@ -314,7 +318,31 @@

    The byte representing the end of a WebSocket text frame.

    -
    See Also:
    Constant Field Values
    +
    See Also:
    Constant Field Values + +
    + +

    +MAX_KEY_VALUE

    +
    +public static final java.lang.Long MAX_KEY_VALUE
    +
    +
    The maximum value of a WebSocket key. +

    +

    +
    +
    +
    + +

    +UTF8_CHARSET

    +
    +public static final java.nio.charset.Charset UTF8_CHARSET
    +
    +
    The WebSocket protocol expects UTF-8 encoded bytes. +

    +

    +
    @@ -327,20 +355,21 @@

    -

    +

    WebSocket

     public WebSocket(java.nio.channels.SocketChannel socketChannel,
    -                 WebSocketListener listener,
    -                 WebSocketProtocol.ClientServerType wstype)
    + java.util.concurrent.BlockingQueue<java.nio.ByteBuffer> bufferQueue, + net.tootallnate.websocket.WebSocketListener listener, + WebSocketProtocol.ClientServerType wstype)
    -
    Used in WebSocketServer and WebSocketClient. +
    Used in WebSocketServer and WebSocketClient.

    -
    Parameters:
    socketChannel - The SocketChannel instance to read and - write to. The channel should already be registered - with a Selector before construction of this object.
    listener - The WebSocketListener to notify of events when - they occur.
    wstype - The type of WebSocket, client or server.
    +
    Parameters:
    socketChannel - The SocketChannel instance to read and write to. The + channel should already be registered with a Selector before + construction of this object.
    bufferQueue - The Queue that we should use to buffer data that hasn't been sent + to the client yet.
    listener - The WebSocketListener to notify of events when they occur.
    wstype - The type of WebSocket, client or server.
    @@ -353,34 +382,14 @@

    -

    -handleRead

    -
    -public void handleRead()
    -                throws java.io.IOException
    -
    -
    Should be called when a Selector has a key that is writable for this - WebSocket's SocketChannel connection. -

    -

    -
    -
    -
    - -
    Throws: -
    java.io.IOException - When socket related I/O errors occur.
    -
    -
    -
    -

    close

     public void close()
                throws java.io.IOException
    -
    Closes the underlying SocketChannel, and calls the listener's onClose - event handler. +
    Closes the underlying SocketChannel, and calls the listener's onClose event + handler.

    @@ -396,14 +405,16 @@

    send

    -public void send(java.lang.String text)
    -          throws java.io.IOException
    +public boolean send(java.lang.String text) + throws java.io.IOException
    +
    Returns:
    True if all of the text was sent to the client by this thread. + False if some of the text had to be buffered to be sent later.
    Throws:
    java.io.IOException
    @@ -435,13 +446,12 @@

    - + - - - - + + +
    @@ -453,19 +463,19 @@

     PREV CLASS  - NEXT CLASSNEXT CLASS - FRAMES   + FRAMES    NO FRAMES     @@ -473,7 +483,7 @@

    - SUMMARY: NESTED | FIELD | CONSTR | METHOD + SUMMARY: NESTED | FIELD | CONSTR | METHOD DETAIL: FIELD | CONSTR | METHOD diff --git a/doc/WebSocketClient.html b/doc/net/tootallnate/websocket/WebSocketClient.html similarity index 55% rename from doc/WebSocketClient.html rename to doc/net/tootallnate/websocket/WebSocketClient.html index 540c57473..6b20f8c1c 100644 --- a/doc/WebSocketClient.html +++ b/doc/net/tootallnate/websocket/WebSocketClient.html @@ -2,14 +2,14 @@ - + WebSocketClient - + - + @@ -75,9 +74,9 @@ - SUMMARY: NESTED | FIELD | CONSTR | METHOD + SUMMARY: NESTED | FIELD | CONSTR | METHOD -DETAIL: FIELD | CONSTR | METHOD +DETAIL: FIELD | CONSTR | METHOD @@ -86,25 +85,28 @@

    + +net.tootallnate.websocket +
    Class WebSocketClient

     java.lang.Object
    -  extended by WebSocketClient
    +  extended by net.tootallnate.websocket.WebSocketClient
     
    -
    All Implemented Interfaces:
    java.lang.Runnable, WebSocketListener, WebSocketProtocol
    +
    All Implemented Interfaces:
    java.lang.Runnable

    -
    public abstract class WebSocketClient
    extends java.lang.Object
    implements java.lang.Runnable, WebSocketListener
    +
    public abstract class WebSocketClient
    extends java.lang.Object
    implements java.lang.Runnable

    The WebSocketClient is an abstract class that expects a valid "ws://" URI to connect to. When connected, an instance receives important events related to the life of the connection. A subclass must implement - onOpen, onClose, and onMessage to be - useful. An instance can send messages to it's connected server via the + onOpen, onClose, and onMessage to be useful. + An instance can send messages to it's connected server via the send method.

    @@ -124,18 +126,24 @@

    Nested Class Summary - -  - - - + + + - + +
    Nested classes/interfaces inherited from interface WebSocketProtocol
    +static classWebSocketProtocol.ClientServerType + +
    +          The type of WebSocket
    WebSocketProtocol.ClientServerType, WebSocketProtocol.Draft +static classWebSocketProtocol.Draft + +
    +          The version of the WebSocket Internet-Draft
    -  - @@ -143,14 +151,21 @@

    -
    Field Summary
    - - - + + + - + +
    Fields inherited from interface WebSocketProtocol
    +static java.lang.LongMAX_KEY_VALUE + +
    +          The maximum value of a WebSocket key.
    MAX_KEY_VALUE, UTF8_CHARSET +static java.nio.charset.CharsetUTF8_CHARSET + +
    +          The WebSocket protocol expects UTF-8 encoded bytes.
      @@ -163,28 +178,28 @@

    Constructor Summary -WebSocketClient() +WebSocketClient()
              Nullary constructor. -WebSocketClient(java.net.URI serverUri) +WebSocketClient(java.net.URI serverUri)
              Constructs a WebSocketClient instance and sets it to the connect to the specified URI. -WebSocketClient(java.net.URI serverUri, +WebSocketClient(java.net.URI serverUri, java.lang.String subprotocol)
              Constructs a WebSocketClient instance and sets it to the connect to the - specified URI using the specified subprotocol. + specified URI using the specified subprotocol and draft. -WebSocketClient(java.net.URI serverUri, +WebSocketClient(java.net.URI serverUri, java.lang.String subprotocol, java.lang.String draft) @@ -192,6 +207,14 @@

              Constructs a WebSocketClient instance and sets it to the connect to the specified URI using the specified subprotocol and draft. + +WebSocketClient(java.net.URI serverUri, + WebSocketProtocol.Draft draft) + +
    +          Constructs a WebSocketClient instance and sets it to the connect to the + specified URI. +   @@ -205,7 +228,7 @@

     void -close() +close()
              Calls close on the underlying SocketChannel, which in turn @@ -214,16 +237,17 @@

     void -connect() +connect()
              Starts a background thread that attempts and maintains a WebSocket - connection to the URI specified in the constructor or via setURI. + connection to the URI specified in the constructor or via + setURI. - WebSocketProtocol.Draft -getDraft() + WebSocketProtocol.Draft +getDraft()
              Gets the draft that this WebSocketClient supports. @@ -231,7 +255,7 @@

     java.lang.String -getSubProtocol() +getSubProtocol()
              Gets the subprotocol that this WebSocketClient supports. @@ -239,7 +263,7 @@

     java.net.URI -getURI() +getURI()
              Gets the URI that this WebSocketClient is connected to (or should attempt @@ -248,7 +272,7 @@

    abstract  void -onClose() +onClose()
                @@ -256,7 +280,7 @@

     void -onClose(WebSocket conn) +onClose(WebSocket conn)
              Calls subclass' implementation of onClose. @@ -264,8 +288,8 @@

     boolean -onHandshakeRecieved(WebSocket conn, - WebSocketHandshake handshake) +onHandshakeRecieved(WebSocket conn, + WebSocketHandshake handshake)
              Parses the server's handshake to verify that it's a valid WebSocket @@ -274,7 +298,7 @@

    abstract  void -onMessage(java.lang.String message) +onMessage(java.lang.String message)
                @@ -282,7 +306,7 @@

     void -onMessage(WebSocket conn, +onMessage(WebSocket conn, java.lang.String message)
    @@ -291,7 +315,7 @@

    abstract  void -onOpen() +onOpen()
                @@ -299,7 +323,7 @@

     void -onOpen(WebSocket conn) +onOpen(WebSocket conn)
              Calls subclass' implementation of onOpen. @@ -307,7 +331,7 @@

     void -run() +run()
                @@ -315,7 +339,7 @@

     void -send(java.lang.String text) +send(java.lang.String text)
              Sends text to the connected WebSocket server. @@ -323,7 +347,7 @@

     void -setDraft(WebSocketProtocol.Draft draft) +setDraft(WebSocketProtocol.Draft draft)
              Sets this WebSocketClient draft. @@ -331,7 +355,7 @@

     void -setSubProtocol(java.lang.String subprotocol) +setSubProtocol(java.lang.String subprotocol)
              Sets this WebSocketClient subprotocol. @@ -339,7 +363,7 @@

     void -setURI(java.net.URI uri) +setURI(java.net.URI uri)
              Sets this WebSocketClient to connect to the specified URI. @@ -357,6 +381,39 @@

     

    + + + + + + + +
    +Field Detail
    + +

    +MAX_KEY_VALUE

    +
    +public static final java.lang.Long MAX_KEY_VALUE
    +
    +
    The maximum value of a WebSocket key. +

    +

    +
    +
    +
    + +

    +UTF8_CHARSET

    +
    +public static final java.nio.charset.Charset UTF8_CHARSET
    +
    +
    The WebSocket protocol expects UTF-8 encoded bytes. +

    +

    +
    +
    + @@ -392,6 +449,21 @@


    +

    +WebSocketClient

    +
    +public WebSocketClient(java.net.URI serverUri,
    +                       WebSocketProtocol.Draft draft)
    +
    +
    Constructs a WebSocketClient instance and sets it to the connect to the + specified URI. The client does not attempt to connect automatically. You + must call connect first to initiate the socket connection. +

    +

    +
    Parameters:
    serverUri -
    +
    +
    +

    WebSocketClient

    @@ -399,12 +471,12 @@ 

    java.lang.String subprotocol)

    Constructs a WebSocketClient instance and sets it to the connect to the - specified URI using the specified subprotocol. The client does not - attempt to connect automatically. You must call connect first - to initiate the socket connection. + specified URI using the specified subprotocol and draft. The client does not attempt + to connect automatically. You must call connect first to + initiate the socket connection.

    -
    Parameters:
    serverUri -
    subprotocol -
    +
    Parameters:
    serverUri -
    subprotocol -
    draft -

    @@ -416,9 +488,9 @@

    java.lang.String draft)
    Constructs a WebSocketClient instance and sets it to the connect to the - specified URI using the specified subprotocol and draft. The client does not - attempt to connect automatically. You must call connect first - to initiate the socket connection. + specified URI using the specified subprotocol and draft. The client does + not attempt to connect automatically. You must call connect + first to initiate the socket connection.

    Parameters:
    serverUri -
    subprotocol -
    draft - DRAFT75 or DRAFT76
    @@ -434,10 +506,10 @@

    -

    +

    setDraft

    -public void setDraft(WebSocketProtocol.Draft draft)
    +public void setDraft(WebSocketProtocol.Draft draft)
    Sets this WebSocketClient draft.

    @@ -453,7 +525,7 @@

    getDraft

    -public WebSocketProtocol.Draft getDraft()
    +public WebSocketProtocol.Draft getDraft()
    Gets the draft that this WebSocketClient supports.

    @@ -506,7 +578,7 @@

    public void setURI(java.net.URI uri)
    Sets this WebSocketClient to connect to the specified URI. - + TODO: Throw an exception if this is called while the socket thread is running.

    @@ -543,8 +615,8 @@

    public void connect()
    Starts a background thread that attempts and maintains a WebSocket - connection to the URI specified in the constructor or via setURI. - setURI. + connection to the URI specified in the constructor or via + setURI. setURI.

    @@ -608,40 +680,42 @@


    -

    +

    onHandshakeRecieved

    -public boolean onHandshakeRecieved(WebSocket conn,
    -                                   WebSocketHandshake handshake)
    -                            throws java.io.IOException
    +public boolean onHandshakeRecieved(WebSocket conn, + WebSocketHandshake handshake) + throws java.io.IOException, + java.security.NoSuchAlgorithmException
    Parses the server's handshake to verify that it's a valid WebSocket handshake.

    -
    Specified by:
    onHandshakeRecieved in interface WebSocketListener
    +
    -
    Parameters:
    conn - The WebSocket instance who's handshake has been recieved. - In the case of WebSocketClient, this.conn == conn.
    handshake - The entire UTF-8 decoded handshake from the connection. +
    Parameters:
    conn - The WebSocket instance who's handshake has been recieved. + In the case of WebSocketClient, this.conn == conn.
    handshake - The entire UTF-8 decoded handshake from the connection.
    Returns:
    true if handshake is a valid WebSocket server handshake, false otherwise.
    Throws: -
    java.io.IOException - When socket related I/O errors occur.
    +
    java.io.IOException - When socket related I/O errors occur. +
    java.security.NoSuchAlgorithmException


    -

    +

    onMessage

    -public void onMessage(WebSocket conn,
    +public void onMessage(WebSocket conn,
                           java.lang.String message)
    Calls subclass' implementation of onMessage.

    -
    Specified by:
    onMessage in interface WebSocketListener
    +
    Parameters:
    conn -
    message -
    @@ -649,15 +723,15 @@


    -

    +

    onOpen

    -public void onOpen(WebSocket conn)
    +public void onOpen(WebSocket conn)
    Calls subclass' implementation of onOpen.

    -
    Specified by:
    onOpen in interface WebSocketListener
    +
    Parameters:
    conn -
    @@ -665,15 +739,15 @@


    -

    +

    onClose

    -public void onClose(WebSocket conn)
    +public void onClose(WebSocket conn)
    Calls subclass' implementation of onClose.

    -
    Specified by:
    onClose in interface WebSocketListener
    +
    Parameters:
    conn -
    @@ -734,13 +808,12 @@

    - + - - - - + + +
    @@ -751,20 +824,20 @@

    PREV CLASS  - NEXT CLASSPREV CLASS  + NEXT CLASS - FRAMES   + FRAMES    NO FRAMES     @@ -772,9 +845,9 @@

    - SUMMARY: NESTED | FIELD | CONSTR | METHOD + SUMMARY: NESTED | FIELD | CONSTR | METHOD -DETAIL: FIELD | CONSTR | METHOD +DETAIL: FIELD | CONSTR | METHOD diff --git a/doc/WebSocketHandshake.html b/doc/net/tootallnate/websocket/WebSocketHandshake.html similarity index 50% rename from doc/WebSocketHandshake.html rename to doc/net/tootallnate/websocket/WebSocketHandshake.html index 4b539a87a..7472f1676 100644 --- a/doc/WebSocketHandshake.html +++ b/doc/net/tootallnate/websocket/WebSocketHandshake.html @@ -2,14 +2,14 @@ - + WebSocketHandshake - + - + @@ -75,9 +74,9 @@ - SUMMARY: NESTED | FIELD | CONSTR | METHOD + SUMMARY: NESTED | FIELD | CONSTR | METHOD -DETAIL: FIELD | CONSTR | METHOD +DETAIL: FIELD | CONSTR | METHOD @@ -86,30 +85,34 @@

    + +net.tootallnate.websocket +
    Class WebSocketHandshake

     java.lang.Object
    -  extended by java.util.Dictionary<K,V>
    -      extended by java.util.Hashtable<java.lang.String,java.lang.Object>
    -          extended by WebSocketHandshake
    +  extended by java.util.Dictionary<K,V>
    +      extended by java.util.Hashtable<java.lang.String,java.lang.Object>
    +          extended by net.tootallnate.websocket.WebSocketHandshake
     
    -
    All Implemented Interfaces:
    java.io.Serializable, java.lang.Cloneable, java.util.Map<java.lang.String,java.lang.Object>, WebSocketProtocol
    +
    All Implemented Interfaces:
    java.io.Serializable, java.lang.Cloneable, java.util.Map<java.lang.String,java.lang.Object>

    -
    public class WebSocketHandshake
    extends java.util.Hashtable<java.lang.String,java.lang.Object>
    implements WebSocketProtocol
    +
    public class WebSocketHandshake
    extends java.util.Hashtable<java.lang.String,java.lang.Object>

    -The WebSocketHandshake is a class used to create and read client and server handshakes. +The WebSocketHandshake is a class used to create and read client and + server handshakes.

    Author:
    Nathan Mishe
    -
    See Also:
    Serialized Form
    +
    See Also:
    Serialized Form

    @@ -121,18 +124,24 @@

    Nested Class Summary - -  - - - + + + - + +
    Nested classes/interfaces inherited from interface WebSocketProtocol
    +static classWebSocketProtocol.ClientServerType + +
    +          The type of WebSocket
    WebSocketProtocol.ClientServerType, WebSocketProtocol.Draft +static classWebSocketProtocol.Draft + +
    +          The version of the WebSocket Internet-Draft
    -  - @@ -140,14 +149,21 @@

    -
    Field Summary
    - - - + + + - + +
    Fields inherited from interface WebSocketProtocol
    +static java.lang.LongMAX_KEY_VALUE + +
    +          The maximum value of a WebSocket key.
    MAX_KEY_VALUE, UTF8_CHARSET +static java.nio.charset.CharsetUTF8_CHARSET + +
    +          The WebSocket protocol expects UTF-8 encoded bytes.
      @@ -160,21 +176,21 @@

    Constructor Summary -WebSocketHandshake() +WebSocketHandshake()
                -WebSocketHandshake(byte[] handshake) +WebSocketHandshake(byte[] handshake)
                -WebSocketHandshake(byte[] handshake, - WebSocketProtocol.ClientServerType type, - WebSocketProtocol.Draft draft) +WebSocketHandshake(byte[] handshake, + WebSocketProtocol.ClientServerType type, + WebSocketProtocol.Draft draft)
                @@ -192,24 +208,24 @@

     void -buildHandshake() +buildHandshake()
    -          Generates the handshake byte array based on the handshake draft and type as well as the keys - set for this handshake. +          Generates the handshake byte array based on the handshake draft + and type as well as the keys set for this handshake.  byte[] -getAsByteArray(java.lang.String fieldName) +getAsByteArray(java.lang.String fieldName)
                - WebSocketProtocol.Draft -getDraft() + WebSocketProtocol.Draft +getDraft()
                @@ -217,7 +233,7 @@

     byte[] -getHandshake() +getHandshake()
                @@ -225,15 +241,15 @@

     java.lang.String -getProperty(java.lang.String fieldName) +getProperty(java.lang.String fieldName)
                - WebSocketProtocol.ClientServerType -getType() + WebSocketProtocol.ClientServerType +getType()
                @@ -241,7 +257,7 @@

     void -parseHandshake() +parseHandshake()
              Parses the parts of the handshake into Hashtable keys. @@ -249,7 +265,7 @@

     void -setDraft(WebSocketProtocol.Draft handshakeDraft) +setDraft(WebSocketProtocol.Draft handshakeDraft)
                @@ -257,7 +273,7 @@

     void -setHandshake(byte[] handshake) +setHandshake(byte[] handshake)
                @@ -265,7 +281,7 @@

     void -setType(WebSocketProtocol.ClientServerType handshakeType) +setType(WebSocketProtocol.ClientServerType handshakeType)
                @@ -273,7 +289,7 @@

     java.lang.String -toString() +toString()
                @@ -300,6 +316,39 @@

     

    + + + + + + + +
    +Field Detail
    + +

    +MAX_KEY_VALUE

    +
    +public static final java.lang.Long MAX_KEY_VALUE
    +
    +
    The maximum value of a WebSocket key. +

    +

    +
    +
    +
    + +

    +UTF8_CHARSET

    +
    +public static final java.nio.charset.Charset UTF8_CHARSET
    +
    +
    The WebSocket protocol expects UTF-8 encoded bytes. +

    +

    +
    +
    + @@ -326,12 +375,12 @@


    -

    +

    WebSocketHandshake

     public WebSocketHandshake(byte[] handshake,
    -                          WebSocketProtocol.ClientServerType type,
    -                          WebSocketProtocol.Draft draft)
    + WebSocketProtocol.ClientServerType type, + WebSocketProtocol.Draft draft)
    @@ -345,10 +394,10 @@

    -

    +

    setDraft

    -public void setDraft(WebSocketProtocol.Draft handshakeDraft)
    +public void setDraft(WebSocketProtocol.Draft handshakeDraft)
    @@ -362,7 +411,7 @@

    getDraft

    -public WebSocketProtocol.Draft getDraft()
    +public WebSocketProtocol.Draft getDraft()
    @@ -401,10 +450,10 @@


    -

    +

    setType

    -public void setType(WebSocketProtocol.ClientServerType handshakeType)
    +public void setType(WebSocketProtocol.ClientServerType handshakeType)
    @@ -418,7 +467,7 @@

    getType

    -public WebSocketProtocol.ClientServerType getType()
    +public WebSocketProtocol.ClientServerType getType()
    @@ -476,10 +525,10 @@

     public void parseHandshake()
    -
    Parses the parts of the handshake into Hashtable keys. Can only be - called if the handshake draft and type are set. Called automatically - when handshake is set, if the draft and type are known and set for - the handshake. +
    Parses the parts of the handshake into Hashtable keys. Can only be called + if the handshake draft and type are set. Called automatically when + handshake is set, if the draft and type are known and + set for the handshake.

    @@ -495,10 +544,10 @@

     public void buildHandshake()
    -
    Generates the handshake byte array based on the handshake draft and type as well as the keys - set for this handshake. Called automatically - when handshake is requested, if the draft and type are known and set for - the handshake. +
    Generates the handshake byte array based on the handshake draft + and type as well as the keys set for this handshake. Called + automatically when handshake is requested, if the draft and + type are known and set for the handshake.

    @@ -520,13 +569,12 @@

    - + - - - - + + +
    @@ -537,20 +585,20 @@

    PREV CLASS  - NEXT CLASSPREV CLASS  + NEXT CLASS - FRAMES   + FRAMES    NO FRAMES     @@ -558,9 +606,9 @@

    - SUMMARY: NESTED | FIELD | CONSTR | METHOD + SUMMARY: NESTED | FIELD | CONSTR | METHOD -DETAIL: FIELD | CONSTR | METHOD +DETAIL: FIELD | CONSTR | METHOD diff --git a/doc/WebSocketServer.html b/doc/net/tootallnate/websocket/WebSocketServer.html similarity index 54% rename from doc/WebSocketServer.html rename to doc/net/tootallnate/websocket/WebSocketServer.html index 3c8ae301b..ce8ef6271 100644 --- a/doc/WebSocketServer.html +++ b/doc/net/tootallnate/websocket/WebSocketServer.html @@ -2,14 +2,14 @@ - + WebSocketServer - + - + @@ -75,7 +74,7 @@ - SUMMARY: NESTED | FIELD | CONSTR | METHOD + SUMMARY: NESTED | FIELD | CONSTR | METHOD DETAIL: FIELD | CONSTR | METHOD @@ -86,17 +85,20 @@

    + +net.tootallnate.websocket +
    Class WebSocketServer

     java.lang.Object
    -  extended by WebSocketServer
    +  extended by net.tootallnate.websocket.WebSocketServer
     
    -
    All Implemented Interfaces:
    java.lang.Runnable, WebSocketListener, WebSocketProtocol
    +
    All Implemented Interfaces:
    java.lang.Runnable

    -
    public abstract class WebSocketServer
    extends java.lang.Object
    implements java.lang.Runnable, WebSocketListener
    +
    public abstract class WebSocketServer
    extends java.lang.Object
    implements java.lang.Runnable

    @@ -104,11 +106,11 @@

    HTTP handshake portion of WebSockets. It's up to a subclass to add functionality/purpose to the server. - May be configured to listen to listen on a specified - port using a specified subprotocol. - May also be configured to only allows connections from a specified origin. - Can be configure to support a specific draft of the WebSocket protocol - (DRAFT75 or DRAFT76) or both (AUTO). + May be configured to listen to listen on a specified port using a + specified subprotocol. May also be configured to only allows + connections from a specified origin. Can be configure to support a + specific draft of the WebSocket protocol (DRAFT75 or DRAFT76) or + both (AUTO).

    @@ -127,18 +129,24 @@

    Nested Class Summary - -  - - - + + + - + +
    Nested classes/interfaces inherited from interface WebSocketProtocol
    +static classWebSocketProtocol.ClientServerType + +
    +          The type of WebSocket
    WebSocketProtocol.ClientServerType, WebSocketProtocol.Draft +static classWebSocketProtocol.Draft + +
    +          The version of the WebSocket Internet-Draft
    -  - @@ -148,39 +156,46 @@

    - + + will be the draft of the WebSocket protocl the WebSocketServer supports. - +          If the nullary constructor is used, the DEFAULT_PORT will be the port the + WebSocketServer is binded to. - -
    -static WebSocketProtocol.DraftDEFAULT_DRAFT +static WebSocketProtocol.DraftDEFAULT_DRAFT
              If a constructor is used that doesn't define the draft, the DEFAULT_DRAFT - will be the draft of the WebSocket protocl the WebSocketServer supports.
    static intDEFAULT_PORT +DEFAULT_PORT
    -          If the nullary constructor is used, the DEFAULT_PORT will be the port - the WebSocketServer is binded to.
    static java.lang.StringFLASH_POLICY_REQUEST +FLASH_POLICY_REQUEST
              The value of handshake when a Flash client requests a policy file on this server.
    - - - + + + - + +
    Fields inherited from interface WebSocketProtocol
    +static java.lang.LongMAX_KEY_VALUE + +
    +          The maximum value of a WebSocket key.
    MAX_KEY_VALUE, UTF8_CHARSET +static java.nio.charset.CharsetUTF8_CHARSET + +
    +          The WebSocket protocol expects UTF-8 encoded bytes.
      @@ -193,20 +208,20 @@

    Constructor Summary -WebSocketServer() +WebSocketServer()
              Nullary constructor. -WebSocketServer(int port) +WebSocketServer(int port)
              Creates a WebSocketServer that will attempt to listen on port port. -WebSocketServer(int port, +WebSocketServer(int port, java.lang.String origin)
    @@ -214,7 +229,7 @@

    port. -WebSocketServer(int port, +WebSocketServer(int port, java.lang.String origin, java.lang.String subprotocol) @@ -223,7 +238,7 @@

    port using a specified subprotocol. -WebSocketServer(int port, +WebSocketServer(int port, java.lang.String origin, java.lang.String subprotocol, java.lang.String draft) @@ -233,10 +248,10 @@

    port using a specified subprotocol. -WebSocketServer(int port, +WebSocketServer(int port, java.lang.String origin, java.lang.String subprotocol, - WebSocketProtocol.Draft draft) + WebSocketProtocol.Draft draft)
              Creates a WebSocketServer that will attempt to listen on port @@ -254,16 +269,16 @@

    - WebSocket[] -connections() + WebSocket[] +connections()
              Returns a WebSocket[] of currently connected clients. - WebSocketProtocol.Draft -getDraft() + WebSocketProtocol.Draft +getDraft()
              Gets the draft that this WebSocketClient supports. @@ -271,7 +286,7 @@

    protected  java.lang.String -getFlashSecurityPolicy() +getFlashSecurityPolicy()
              Gets the XML string that should be returned if a client requests a Flash @@ -280,16 +295,15 @@

     java.lang.String -getOrigin() +getOrigin()
    -          Gets the origin that this WebSocketServer should allow connections - from. +          Gets the origin that this WebSocketServer should allow connections from.  int -getPort() +getPort()
              Gets the port number that this server listens on. @@ -297,15 +311,23 @@

     java.lang.String -getSubProtocol() +getSubProtocol()
              Gets the subprotocol that this WebSocketClient supports. +protected  java.util.concurrent.BlockingQueue<java.nio.ByteBuffer> +newBufferQueue() + +
    +            + + + abstract  void -onClientClose(WebSocket conn) +onClientClose(WebSocket conn)
                @@ -313,7 +335,7 @@

    abstract  void -onClientMessage(WebSocket conn, +onClientMessage(WebSocket conn, java.lang.String message)
    @@ -322,7 +344,7 @@

    abstract  void -onClientOpen(WebSocket conn) +onClientOpen(WebSocket conn)
                @@ -330,7 +352,7 @@

     void -onClose(WebSocket conn) +onClose(WebSocket conn)
              Called after WebSocket#close is explicity called, or when the @@ -339,17 +361,17 @@

     boolean -onHandshakeRecieved(WebSocket conn, - WebSocketHandshake handshake) +onHandshakeRecieved(WebSocket conn, + WebSocketHandshake handshake)
    -          Called by a WebSocket instance when a client connection has +          Called by a WebSocket instance when a client connection has finished sending a handshake.  void -onMessage(WebSocket conn, +onMessage(WebSocket conn, java.lang.String message)
    @@ -358,7 +380,7 @@

     void -onOpen(WebSocket conn) +onOpen(WebSocket conn)
              Called after onHandshakeRecieved returns true. @@ -366,7 +388,7 @@

     void -run() +run()
                @@ -374,7 +396,27 @@

     void -sendToAll(java.lang.String text) +sendTo(java.util.Set<WebSocket> connections, + java.lang.String text) + +
    +          Sends text to all currently connected WebSocket clients found in + the Set connections. + + + + void +sendTo(WebSocket connection, + java.lang.String text) + +
    +          Sends text to connected WebSocket client specified by + connection. + + + + void +sendToAll(java.lang.String text)
              Sends text to all currently connected WebSocket clients. @@ -382,27 +424,27 @@

     void -sendToAllExcept(java.util.Set<WebSocket> connections, +sendToAllExcept(java.util.Set<WebSocket> connections, java.lang.String text)
    -          Sends text to all currently connected WebSocket clients, - except for those found in the Set connections. +          Sends text to all currently connected WebSocket clients, except + for those found in the Set connections.  void -sendToAllExcept(WebSocket connection, +sendToAllExcept(WebSocket connection, java.lang.String text)
    -          Sends text to all currently connected WebSocket clients, - except for the specified connection. +          Sends text to all currently connected WebSocket clients, except + for the specified connection.  void -setDraft(WebSocketProtocol.Draft draft) +setDraft(WebSocketProtocol.Draft draft)
              Sets this WebSocketClient draft. @@ -410,16 +452,15 @@

     void -setOrigin(java.lang.String origin) +setOrigin(java.lang.String origin)
    -          Sets the origin that this WebSocketServer should allow connections - from. +          Sets the origin that this WebSocketServer should allow connections from.  void -setPort(int port) +setPort(int port)
              Sets the port that this WebSocketServer should listen on. @@ -427,7 +468,7 @@

     void -setSubProtocol(java.lang.String subprotocol) +setSubProtocol(java.lang.String subprotocol)
              Sets this WebSocketClient subprotocol. @@ -435,7 +476,7 @@

     void -start() +start()
              Starts the server thread that binds to the currently set port number and @@ -444,7 +485,7 @@

     void -stop() +stop()
              Closes all connected clients sockets, then closes the underlying @@ -479,21 +520,21 @@

     public static final int DEFAULT_PORT
    -
    If the nullary constructor is used, the DEFAULT_PORT will be the port - the WebSocketServer is binded to. +
    If the nullary constructor is used, the DEFAULT_PORT will be the port the + WebSocketServer is binded to.

    -
    See Also:
    Constant Field Values
    +
    See Also:
    Constant Field Values


    DEFAULT_DRAFT

    -public static final WebSocketProtocol.Draft DEFAULT_DRAFT
    +public static final WebSocketProtocol.Draft DEFAULT_DRAFT
    If a constructor is used that doesn't define the draft, the DEFAULT_DRAFT - will be the draft of the WebSocket protocl the WebSocketServer supports. + will be the draft of the WebSocket protocl the WebSocketServer supports.

    @@ -509,7 +550,31 @@

    file on this server.

    -
    See Also:
    Constant Field Values
    +
    See Also:
    Constant Field Values

    +

    +
    + +

    +MAX_KEY_VALUE

    +
    +public static final java.lang.Long MAX_KEY_VALUE
    +
    +
    The maximum value of a WebSocket key. +

    +

    +
    +
    +
    + +

    +UTF8_CHARSET

    +
    +public static final java.nio.charset.Charset UTF8_CHARSET
    +
    +
    The WebSocket protocol expects UTF-8 encoded bytes. +

    +

    +
    @@ -527,9 +592,9 @@

     public WebSocketServer()
    -
    Nullary constructor. Creates a WebSocketServer that will attempt to - listen on port DEFAULT_PORT and support both draft 75 and 76 of the - WebSocket protocol. +
    Nullary constructor. Creates a WebSocketServer that will attempt to listen + on port DEFAULT_PORT and support both draft 75 and 76 of the WebSocket + protocol.


    @@ -569,8 +634,8 @@

    java.lang.String subprotocol)
    Creates a WebSocketServer that will attempt to listen on port - port using a specified subprotocol. - Only allows connections from origin. + port using a specified subprotocol. Only allows + connections from origin.

    Parameters:
    port - The port number this server should listen on.
    origin - The origin this server supports.
    subprotocol - The subprotocol this server supports.
    @@ -586,27 +651,27 @@

    java.lang.String draft)
    Creates a WebSocketServer that will attempt to listen on port - port using a specified subprotocol. - Only allows connections from origin using specified - draft of the WebSocket protocol. + port using a specified subprotocol. Only allows + connections from origin using specified draft of the + WebSocket protocol.

    Parameters:
    port - The port number this server should listen on.
    origin - The origin this server supports.
    subprotocol - The subprotocol this server supports.
    draft - The draft of the WebSocket protocol this server supports.

    -

    +

    WebSocketServer

     public WebSocketServer(int port,
                            java.lang.String origin,
                            java.lang.String subprotocol,
    -                       WebSocketProtocol.Draft draft)
    + WebSocketProtocol.Draft draft)
    Creates a WebSocketServer that will attempt to listen on port - port using a specified subprotocol. - Only allows connections from origin using specified - draft of the WebSocket protocol. + port using a specified subprotocol. Only allows + connections from origin using specified draft of the + WebSocket protocol.

    Parameters:
    port - The port number this server should listen on.
    origin - The origin this server supports.
    subprotocol - The subprotocol this server supports.
    draft - The draft of the WebSocket protocol this server supports.
    @@ -622,10 +687,10 @@

    -

    +

    setDraft

    -public void setDraft(WebSocketProtocol.Draft draft)
    +public void setDraft(WebSocketProtocol.Draft draft)
    Sets this WebSocketClient draft.

    @@ -641,7 +706,7 @@

    getDraft

    -public WebSocketProtocol.Draft getDraft()
    +public WebSocketProtocol.Draft getDraft()
    Gets the draft that this WebSocketClient supports.

    @@ -660,8 +725,7 @@

     public void setOrigin(java.lang.String origin)
    -
    Sets the origin that this WebSocketServer should allow connections - from. +
    Sets the origin that this WebSocketServer should allow connections from.

    @@ -677,8 +741,7 @@

     public java.lang.String getOrigin()
    -
    Gets the origin that this WebSocketServer should allow connections - from. +
    Gets the origin that this WebSocketServer should allow connections from.

    @@ -794,6 +857,48 @@


    +

    +sendTo

    +
    +public void sendTo(WebSocket connection,
    +                   java.lang.String text)
    +            throws java.io.IOException
    +
    +
    Sends text to connected WebSocket client specified by + connection. +

    +

    +
    +
    +
    +
    Parameters:
    connection - The WebSocket connection to send to.
    text - The String to send to connection. +
    Throws: +
    java.io.IOException - When socket related I/O errors occur.
    +
    +
    +
    + +

    +sendTo

    +
    +public void sendTo(java.util.Set<WebSocket> connections,
    +                   java.lang.String text)
    +            throws java.io.IOException
    +
    +
    Sends text to all currently connected WebSocket clients found in + the Set connections. +

    +

    +
    +
    +
    +
    Parameters:
    connections -
    text - +
    Throws: +
    java.io.IOException - When socket related I/O errors occur.
    +
    +
    +
    +

    sendToAll

    @@ -813,21 +918,22 @@ 


    -

    +

    sendToAllExcept

    -public void sendToAllExcept(WebSocket connection,
    +public void sendToAllExcept(WebSocket connection,
                                 java.lang.String text)
                          throws java.io.IOException
    -
    Sends text to all currently connected WebSocket clients, - except for the specified connection. +
    Sends text to all currently connected WebSocket clients, except + for the specified connection.

    -
    Parameters:
    connection - The WebSocket connection to ignore.
    text - The String to send to every connection except connection. +
    Parameters:
    connection - The WebSocket connection to ignore.
    text - The String to send to every connection except + connection.
    Throws:
    java.io.IOException - When socket related I/O errors occur.
    @@ -837,12 +943,12 @@

    sendToAllExcept

    -public void sendToAllExcept(java.util.Set<WebSocket> connections,
    +public void sendToAllExcept(java.util.Set<WebSocket> connections,
                                 java.lang.String text)
                          throws java.io.IOException
    -
    Sends text to all currently connected WebSocket clients, - except for those found in the Set connections. +
    Sends text to all currently connected WebSocket clients, except + for those found in the Set connections.

    @@ -858,7 +964,7 @@

    connections

    -public WebSocket[] connections()
    +public WebSocket[] connections()
    Returns a WebSocket[] of currently connected clients.

    @@ -872,6 +978,25 @@


    +

    +newBufferQueue

    +
    +protected java.util.concurrent.BlockingQueue<java.nio.ByteBuffer> newBufferQueue()
    +
    +
    +
    +
    +
    + +
    Returns:
    A BlockingQueue that should be used by a WebSocket to hold data + that is waiting to be sent to the client. The default + implementation returns an unbounded LinkedBlockingQueue, but you + may choose to override this to return a bounded queue to protect + against running out of memory.
    +
    +
    +
    +

    run

    @@ -893,12 +1018,12 @@ 

    Gets the XML string that should be returned if a client requests a Flash security policy. - - The default implementation allows access from all remote domains, but - only on the port that this WebSocketServer is listening on. - + + The default implementation allows access from all remote domains, but only + on the port that this WebSocketServer is listening on. + This is specifically implemented for gitime's WebSocket client for Flash: - http://github.com/gimite/web-socket-js + http://github.com/gimite/web-socket-js

    @@ -912,23 +1037,23 @@


    -

    +

    onHandshakeRecieved

    -public boolean onHandshakeRecieved(WebSocket conn,
    -                                   WebSocketHandshake handshake)
    +public boolean onHandshakeRecieved(WebSocket conn,
    +                                   WebSocketHandshake handshake)
                                 throws java.io.IOException
    -
    Called by a WebSocket instance when a client connection has - finished sending a handshake. This method verifies that the handshake is - a valid WebSocket cliend request. Then sends a WebSocket server handshake - if it is valid, or closes the connection if it is not. +
    Called by a WebSocket instance when a client connection has + finished sending a handshake. This method verifies that the handshake is a + valid WebSocket cliend request. Then sends a WebSocket server handshake if + it is valid, or closes the connection if it is not.

    -
    Specified by:
    onHandshakeRecieved in interface WebSocketListener
    +
    -
    Parameters:
    conn - The WebSocket instance who's handshake has been recieved.
    handshake - The entire UTF-8 decoded handshake from the connection. +
    Parameters:
    conn - The WebSocket instance who's handshake has been recieved.
    handshake - The entire UTF-8 decoded handshake from the connection.
    Returns:
    True if the client sent a valid WebSocket handshake and this server successfully sent a WebSocket server handshake, false otherwise.
    Throws: @@ -937,18 +1062,17 @@


    -

    +

    onMessage

    -public void onMessage(WebSocket conn,
    +public void onMessage(WebSocket conn,
                           java.lang.String message)
    -
    Description copied from interface: WebSocketListener
    Called when an entire text frame has been recieved. Do whatever you want here...

    -
    Specified by:
    onMessage in interface WebSocketListener
    +
    Parameters:
    conn - The WebSocket instance this event is occuring on.
    message - The UTF-8 decoded message that was recieved.
    @@ -956,18 +1080,17 @@


    -

    +

    onOpen

    -public void onOpen(WebSocket conn)
    +public void onOpen(WebSocket conn)
    -
    Description copied from interface: WebSocketListener
    Called after onHandshakeRecieved returns true. - Indicates that a complete WebSocket connection has been established, - and we are ready to send/recieve data. + Indicates that a complete WebSocket connection has been established, and we + are ready to send/recieve data.

    -
    Specified by:
    onOpen in interface WebSocketListener
    +
    Parameters:
    conn - The WebSocket instance this event is occuring on.
    @@ -975,17 +1098,16 @@


    -

    +

    onClose

    -public void onClose(WebSocket conn)
    +public void onClose(WebSocket conn)
    -
    Description copied from interface: WebSocketListener
    Called after WebSocket#close is explicity called, or when the other end of the WebSocket connection is closed.

    -
    Specified by:
    onClose in interface WebSocketListener
    +
    Parameters:
    conn - The WebSocket instance this event is occuring on.
    @@ -993,10 +1115,10 @@


    -

    +

    onClientOpen

    -public abstract void onClientOpen(WebSocket conn)
    +public abstract void onClientOpen(WebSocket conn)
    @@ -1007,10 +1129,10 @@


    -

    +

    onClientClose

    -public abstract void onClientClose(WebSocket conn)
    +public abstract void onClientClose(WebSocket conn)
    @@ -1021,10 +1143,10 @@


    -

    +

    onClientMessage

    -public abstract void onClientMessage(WebSocket conn,
    +public abstract void onClientMessage(WebSocket conn,
                                          java.lang.String message)
    @@ -1047,13 +1169,12 @@

    - + - - - - + + +
    @@ -1064,20 +1185,20 @@

    PREV CLASS  + PREV CLASS   NEXT CLASS - FRAMES   + FRAMES    NO FRAMES     @@ -1085,7 +1206,7 @@

    - SUMMARY: NESTED | FIELD | CONSTR | METHOD + SUMMARY: NESTED | FIELD | CONSTR | METHOD DETAIL: FIELD | CONSTR | METHOD diff --git a/doc/net/tootallnate/websocket/package-frame.html b/doc/net/tootallnate/websocket/package-frame.html new file mode 100644 index 000000000..58446a197 --- /dev/null +++ b/doc/net/tootallnate/websocket/package-frame.html @@ -0,0 +1,38 @@ + + + + + + +net.tootallnate.websocket + + + + + + + + + + + +net.tootallnate.websocket + + + + +
    +Classes  + +
    +WebSocket +
    +WebSocketClient +
    +WebSocketHandshake +
    +WebSocketServer
    + + + + diff --git a/doc/net/tootallnate/websocket/package-summary.html b/doc/net/tootallnate/websocket/package-summary.html new file mode 100644 index 000000000..eb8d8e854 --- /dev/null +++ b/doc/net/tootallnate/websocket/package-summary.html @@ -0,0 +1,168 @@ + + + + + + +net.tootallnate.websocket + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +
    +

    +Package net.tootallnate.websocket +

    + + + + + + + + + + + + + + + + + + + + + +
    +Class Summary
    WebSocketRepresents one end (client or server) of a single WebSocket connection.
    WebSocketClientThe WebSocketClient is an abstract class that expects a valid + "ws://" URI to connect to.
    WebSocketHandshakeThe WebSocketHandshake is a class used to create and read client and + server handshakes.
    WebSocketServerWebSocketServer is an abstract class that only takes care of the + HTTP handshake portion of WebSockets.
    +  + +

    +

    +
    +
    + + + + + + + + + + + + + + + +
    + +
    + + + +
    + + + diff --git a/doc/net/tootallnate/websocket/package-tree.html b/doc/net/tootallnate/websocket/package-tree.html new file mode 100644 index 000000000..d293c59a9 --- /dev/null +++ b/doc/net/tootallnate/websocket/package-tree.html @@ -0,0 +1,153 @@ + + + + + + +net.tootallnate.websocket Class Hierarchy + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + +
    + +
    + + + +
    +
    +

    +Hierarchy For Package net.tootallnate.websocket +

    +
    +

    +Class Hierarchy +

    +
      +
    • java.lang.Object
        +
      • java.util.Dictionary<K,V>
          +
        • java.util.Hashtable<K,V> (implements java.lang.Cloneable, java.util.Map<K,V>, java.io.Serializable) + +
        +
      • net.tootallnate.websocket.WebSocket
      • net.tootallnate.websocket.WebSocketClient (implements java.lang.Runnable) +
      • net.tootallnate.websocket.WebSocketServer (implements java.lang.Runnable) +
      +
    +
    + + + + + + + + + + + + + + + +
    + +
    + + + +
    + + + diff --git a/doc/overview-tree.html b/doc/overview-tree.html index 785067d11..1812829c7 100644 --- a/doc/overview-tree.html +++ b/doc/overview-tree.html @@ -2,12 +2,12 @@ - + Class Hierarchy - + @@ -37,12 +37,11 @@ - + - - +
    @@ -82,6 +81,9 @@

    Hierarchy For All Packages

    +
    +
    Package Hierarchies:
    net.tootallnate.websocket
    +

    Class Hierarchy

    @@ -90,29 +92,10 @@

  • java.util.Dictionary<K,V> -
  • WebSocket (implements WebSocketProtocol) -
  • WebSocketClient (implements java.lang.Runnable, WebSocketListener) -
  • WebSocketServer (implements java.lang.Runnable, WebSocketListener) - +
  • net.tootallnate.websocket.WebSocketHandshake -

    -Interface Hierarchy -

    - -

    -Enum Hierarchy -

    -
    @@ -127,12 +110,11 @@

    - + - - +
    diff --git a/doc/package-frame.html b/doc/package-frame.html deleted file mode 100644 index 0798ef96c..000000000 --- a/doc/package-frame.html +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - -<Unnamed> - - - - - - - - - - - -<Unnamed> - - - - -
    -Interfaces  - -
    -WebSocketListener -
    -WebSocketProtocol
    - - - - - - -
    -Classes  - -
    -WebSocket -
    -WebSocketClient -
    -WebSocketHandshake -
    -WebSocketServer
    - - - - - - -
    -Enums  - -
    -WebSocketProtocol.ClientServerType -
    -WebSocketProtocol.Draft
    - - - - diff --git a/doc/package-list b/doc/package-list index 8b1378917..1bdaefc42 100644 --- a/doc/package-list +++ b/doc/package-list @@ -1 +1 @@ - +net.tootallnate.websocket diff --git a/doc/package-summary.html b/doc/package-summary.html deleted file mode 100644 index 6feaf9ec6..000000000 --- a/doc/package-summary.html +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -
    -

    -Package <Unnamed> -

    - - - - - - - - - - - - - -
    -Interface Summary
    WebSocketListenerImplemented by WebSocketClient and WebSocketServer.
    WebSocketProtocolConstants used by WebSocket protocol.
    -  - -

    - - - - - - - - - - - - - - - - - - - - - -
    -Class Summary
    WebSocketRepresents one end (client or server) of a single WebSocket connection.
    WebSocketClientThe WebSocketClient is an abstract class that expects a valid - "ws://" URI to connect to.
    WebSocketHandshakeThe WebSocketHandshake is a class used to create and read client and server handshakes.
    WebSocketServerWebSocketServer is an abstract class that only takes care of the - HTTP handshake portion of WebSockets.
    -  - -

    - - - - - - - - - - - - - -
    -Enum Summary
    WebSocketProtocol.ClientServerTypeThe type of WebSocket
    WebSocketProtocol.DraftThe version of the WebSocket Internet-Draft
    -  - -

    -

    -
    -
    - - - - - - - - - - - - - - - -
    - -
    - - - -
    - - - diff --git a/doc/package-tree.html b/doc/package-tree.html deleted file mode 100644 index d3f677590..000000000 --- a/doc/package-tree.html +++ /dev/null @@ -1,174 +0,0 @@ - - - - - - - Class Hierarchy - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -
    -
    -

    -Hierarchy For Package <Unnamed> -

    -
    -

    -Class Hierarchy -

    - -

    -Interface Hierarchy -

    - -

    -Enum Hierarchy -

    - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -
    - - - diff --git a/doc/package-use.html b/doc/package-use.html deleted file mode 100644 index 7266de237..000000000 --- a/doc/package-use.html +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - -Uses of Package - - - - - - - - - - - - -
    - - - - - - - - - - - - - - - -
    - -
    - - - -
    -
    -

    -Uses of Package

    -
    - - - - - - - - -
    -Classes in <Unnamed> used by <Unnamed>
    WebSocket - -
    -          Represents one end (client or server) of a single WebSocket connection.
    -  -

    -


    - - - - - - - - - - - - - - - -
    - -
    - - - -
    - - - diff --git a/doc/serialized-form.html b/doc/serialized-form.html index 3c4473be6..3352f56f6 100644 --- a/doc/serialized-form.html +++ b/doc/serialized-form.html @@ -2,12 +2,12 @@ - + Serialized Form - + @@ -37,12 +37,11 @@ - + - - + - +
    @@ -87,16 +86,16 @@

    +Packagenet.tootallnate.websocket
    -Package <Unnamed>

    - + +Class net.tootallnate.websocket.WebSocketHandshake extends java.util.Hashtable<java.lang.String,java.lang.Object> implements Serializable
    -Class WebSocketHandshake extends java.util.Hashtable<java.lang.String,java.lang.Object> implements Serializable
    @@ -126,7 +125,7 @@

    handshakeDraft

    -WebSocketProtocol.Draft handshakeDraft
    +WebSocketProtocol.Draft handshakeDraft
    The WebSocket Internet-Draft version.

    @@ -137,10 +136,10 @@

    handshakeType

    -WebSocketProtocol.ClientServerType handshakeType
    +WebSocketProtocol.ClientServerType handshakeType
    -
    The type of the handshake. The refers to the source of the handshake, either - client or server. +
    The type of the handshake. The refers to the source of the handshake, + either client or server.

    @@ -159,12 +158,11 @@

    - + - - + - +
    diff --git a/example/ChatClient.java b/example/ChatClient.java index 16d9326b0..0d1860eb3 100644 --- a/example/ChatClient.java +++ b/example/ChatClient.java @@ -1,4 +1,3 @@ - import java.awt.Container; import java.awt.GridLayout; import java.awt.event.ActionEvent; @@ -13,14 +12,16 @@ import javax.swing.JTextArea; import javax.swing.JTextField; +import net.tootallnate.websocket.WebSocketClient; + /** * A barebones chat client that uses the WebSocket protocol. */ public class ChatClient extends WebSocketClient { private final JTextArea ta; - public ChatClient(URI uri, JTextArea ta) { - super(uri); + public ChatClient(URI uri, JTextArea ta, Draft draft) { + super(uri, draft); this.ta = ta; } @@ -56,7 +57,7 @@ public Frame() { c.setLayout(layout); uriField = new JTextField(); - uriField.setText("ws://localhost"); + uriField.setText("ws://localhost:8887"); c.add(uriField); connect = new JButton("Connect"); @@ -109,7 +110,7 @@ public void actionPerformed(ActionEvent e) { connect.setEnabled(false); uriField.setEditable(false); try { - cc = new ChatClient(new URI(uriField.getText()), area); + cc = new ChatClient(new URI(uriField.getText()), area, Draft.DRAFT76); cc.connect(); } catch (URISyntaxException ex) { area.append(uriField.getText() + " is not a valid WebSocket URI\n"); diff --git a/example/ChatServer.java b/example/ChatServer.java index 987cbe12e..62bce832c 100644 --- a/example/ChatServer.java +++ b/example/ChatServer.java @@ -1,11 +1,14 @@ - import java.io.IOException; +import net.tootallnate.websocket.WebSocket; +import net.tootallnate.websocket.WebSocketServer; + /** * A simple WebSocketServer implementation. Keeps track of a "chatroom". */ public class ChatServer extends WebSocketServer { + public ChatServer(int port, String origin, String subprotocol, Draft draft) { super(port,origin,subprotocol,draft); } @@ -17,6 +20,7 @@ public void onClientOpen(WebSocket conn) { ex.printStackTrace(); } System.out.println(conn + " entered the room!"); + } public void onClientClose(WebSocket conn) { diff --git a/example/FABridge.js b/example/FABridge.js index 971ac3546..05c877c1a 100644 --- a/example/FABridge.js +++ b/example/FABridge.js @@ -1,604 +1,604 @@ -/* -/* -Copyright 2006 Adobe Systems Incorporated - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE -OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -*/ - - -/* - * The Bridge class, responsible for navigating AS instances - */ -function FABridge(target,bridgeName) -{ - this.target = target; - this.remoteTypeCache = {}; - this.remoteInstanceCache = {}; - this.remoteFunctionCache = {}; - this.localFunctionCache = {}; - this.bridgeID = FABridge.nextBridgeID++; - this.name = bridgeName; - this.nextLocalFuncID = 0; - FABridge.instances[this.name] = this; - FABridge.idMap[this.bridgeID] = this; - - return this; -} - -// type codes for packed values -FABridge.TYPE_ASINSTANCE = 1; -FABridge.TYPE_ASFUNCTION = 2; - -FABridge.TYPE_JSFUNCTION = 3; -FABridge.TYPE_ANONYMOUS = 4; - -FABridge.initCallbacks = {}; -FABridge.userTypes = {}; - -FABridge.addToUserTypes = function() -{ - for (var i = 0; i < arguments.length; i++) - { - FABridge.userTypes[arguments[i]] = { - 'typeName': arguments[i], - 'enriched': false - }; - } -} - -FABridge.argsToArray = function(args) -{ - var result = []; - for (var i = 0; i < args.length; i++) - { - result[i] = args[i]; - } - return result; -} - -function instanceFactory(objID) -{ - this.fb_instance_id = objID; - return this; -} - -function FABridge__invokeJSFunction(args) -{ - var funcID = args[0]; - var throughArgs = args.concat();//FABridge.argsToArray(arguments); - throughArgs.shift(); - - var bridge = FABridge.extractBridgeFromID(funcID); - return bridge.invokeLocalFunction(funcID, throughArgs); -} - -FABridge.addInitializationCallback = function(bridgeName, callback) -{ - var inst = FABridge.instances[bridgeName]; - if (inst != undefined) - { - callback.call(inst); - return; - } - - var callbackList = FABridge.initCallbacks[bridgeName]; - if(callbackList == null) - { - FABridge.initCallbacks[bridgeName] = callbackList = []; - } - - callbackList.push(callback); -} - -// updated for changes to SWFObject2 -function FABridge__bridgeInitialized(bridgeName) { - var objects = document.getElementsByTagName("object"); - var ol = objects.length; - var activeObjects = []; - if (ol > 0) { - for (var i = 0; i < ol; i++) { - if (typeof objects[i].SetVariable != "undefined") { - activeObjects[activeObjects.length] = objects[i]; - } - } - } - var embeds = document.getElementsByTagName("embed"); - var el = embeds.length; - var activeEmbeds = []; - if (el > 0) { - for (var j = 0; j < el; j++) { - if (typeof embeds[j].SetVariable != "undefined") { - activeEmbeds[activeEmbeds.length] = embeds[j]; - } - } - } - var aol = activeObjects.length; - var ael = activeEmbeds.length; - var searchStr = "bridgeName="+ bridgeName; - if ((aol == 1 && !ael) || (aol == 1 && ael == 1)) { - FABridge.attachBridge(activeObjects[0], bridgeName); - } - else if (ael == 1 && !aol) { - FABridge.attachBridge(activeEmbeds[0], bridgeName); - } - else { - var flash_found = false; - if (aol > 1) { - for (var k = 0; k < aol; k++) { - var params = activeObjects[k].childNodes; - for (var l = 0; l < params.length; l++) { - var param = params[l]; - if (param.nodeType == 1 && param.tagName.toLowerCase() == "param" && param["name"].toLowerCase() == "flashvars" && param["value"].indexOf(searchStr) >= 0) { - FABridge.attachBridge(activeObjects[k], bridgeName); - flash_found = true; - break; - } - } - if (flash_found) { - break; - } - } - } - if (!flash_found && ael > 1) { - for (var m = 0; m < ael; m++) { - var flashVars = activeEmbeds[m].attributes.getNamedItem("flashVars").nodeValue; - if (flashVars.indexOf(searchStr) >= 0) { - FABridge.attachBridge(activeEmbeds[m], bridgeName); - break; - } - } - } - } - return true; -} - -// used to track multiple bridge instances, since callbacks from AS are global across the page. - -FABridge.nextBridgeID = 0; -FABridge.instances = {}; -FABridge.idMap = {}; -FABridge.refCount = 0; - -FABridge.extractBridgeFromID = function(id) -{ - var bridgeID = (id >> 16); - return FABridge.idMap[bridgeID]; -} - -FABridge.attachBridge = function(instance, bridgeName) -{ - var newBridgeInstance = new FABridge(instance, bridgeName); - - FABridge[bridgeName] = newBridgeInstance; - -/* FABridge[bridgeName] = function() { - return newBridgeInstance.root(); - } -*/ - var callbacks = FABridge.initCallbacks[bridgeName]; - if (callbacks == null) - { - return; - } - for (var i = 0; i < callbacks.length; i++) - { - callbacks[i].call(newBridgeInstance); - } - delete FABridge.initCallbacks[bridgeName] -} - -// some methods can't be proxied. You can use the explicit get,set, and call methods if necessary. - -FABridge.blockedMethods = -{ - toString: true, - get: true, - set: true, - call: true -}; - -FABridge.prototype = -{ - - -// bootstrapping - - root: function() - { - return this.deserialize(this.target.getRoot()); - }, -//clears all of the AS objects in the cache maps - releaseASObjects: function() - { - return this.target.releaseASObjects(); - }, -//clears a specific object in AS from the type maps - releaseNamedASObject: function(value) - { - if(typeof(value) != "object") - { - return false; - } - else - { - var ret = this.target.releaseNamedASObject(value.fb_instance_id); - return ret; - } - }, -//create a new AS Object - create: function(className) - { - return this.deserialize(this.target.create(className)); - }, - - - // utilities - - makeID: function(token) - { - return (this.bridgeID << 16) + token; - }, - - - // low level access to the flash object - -//get a named property from an AS object - getPropertyFromAS: function(objRef, propName) - { - if (FABridge.refCount > 0) - { - throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); - } - else - { - FABridge.refCount++; - retVal = this.target.getPropFromAS(objRef, propName); - retVal = this.handleError(retVal); - FABridge.refCount--; - return retVal; - } - }, -//set a named property on an AS object - setPropertyInAS: function(objRef,propName, value) - { - if (FABridge.refCount > 0) - { - throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); - } - else - { - FABridge.refCount++; - retVal = this.target.setPropInAS(objRef,propName, this.serialize(value)); - retVal = this.handleError(retVal); - FABridge.refCount--; - return retVal; - } - }, - -//call an AS function - callASFunction: function(funcID, args) - { - if (FABridge.refCount > 0) - { - throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); - } - else - { - FABridge.refCount++; - retVal = this.target.invokeASFunction(funcID, this.serialize(args)); - retVal = this.handleError(retVal); - FABridge.refCount--; - return retVal; - } - }, -//call a method on an AS object - callASMethod: function(objID, funcName, args) - { - if (FABridge.refCount > 0) - { - throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); - } - else - { - FABridge.refCount++; - args = this.serialize(args); - retVal = this.target.invokeASMethod(objID, funcName, args); - retVal = this.handleError(retVal); - FABridge.refCount--; - return retVal; - } - }, - - // responders to remote calls from flash - - //callback from flash that executes a local JS function - //used mostly when setting js functions as callbacks on events - invokeLocalFunction: function(funcID, args) - { - var result; - var func = this.localFunctionCache[funcID]; - - if(func != undefined) - { - result = this.serialize(func.apply(null, this.deserialize(args))); - } - - return result; - }, - - // Object Types and Proxies - - // accepts an object reference, returns a type object matching the obj reference. - getTypeFromName: function(objTypeName) - { - return this.remoteTypeCache[objTypeName]; - }, - //create an AS proxy for the given object ID and type - createProxy: function(objID, typeName) - { - var objType = this.getTypeFromName(typeName); - instanceFactory.prototype = objType; - var instance = new instanceFactory(objID); - this.remoteInstanceCache[objID] = instance; - return instance; - }, - //return the proxy associated with the given object ID - getProxy: function(objID) - { - return this.remoteInstanceCache[objID]; - }, - - // accepts a type structure, returns a constructed type - addTypeDataToCache: function(typeData) - { - newType = new ASProxy(this, typeData.name); - var accessors = typeData.accessors; - for (var i = 0; i < accessors.length; i++) - { - this.addPropertyToType(newType, accessors[i]); - } - - var methods = typeData.methods; - for (var i = 0; i < methods.length; i++) - { - if (FABridge.blockedMethods[methods[i]] == undefined) - { - this.addMethodToType(newType, methods[i]); - } - } - - - this.remoteTypeCache[newType.typeName] = newType; - return newType; - }, - - //add a property to a typename; used to define the properties that can be called on an AS proxied object - addPropertyToType: function(ty, propName) - { - var c = propName.charAt(0); - var setterName; - var getterName; - if(c >= "a" && c <= "z") - { - getterName = "get" + c.toUpperCase() + propName.substr(1); - setterName = "set" + c.toUpperCase() + propName.substr(1); - } - else - { - getterName = "get" + propName; - setterName = "set" + propName; - } - ty[setterName] = function(val) - { - this.bridge.setPropertyInAS(this.fb_instance_id, propName, val); - } - ty[getterName] = function() - { - return this.bridge.deserialize(this.bridge.getPropertyFromAS(this.fb_instance_id, propName)); - } - }, - - //add a method to a typename; used to define the methods that can be callefd on an AS proxied object - addMethodToType: function(ty, methodName) - { - ty[methodName] = function() - { - return this.bridge.deserialize(this.bridge.callASMethod(this.fb_instance_id, methodName, FABridge.argsToArray(arguments))); - } - }, - - // Function Proxies - - //returns the AS proxy for the specified function ID - getFunctionProxy: function(funcID) - { - var bridge = this; - if (this.remoteFunctionCache[funcID] == null) - { - this.remoteFunctionCache[funcID] = function() - { - bridge.callASFunction(funcID, FABridge.argsToArray(arguments)); - } - } - return this.remoteFunctionCache[funcID]; - }, - - //reutrns the ID of the given function; if it doesnt exist it is created and added to the local cache - getFunctionID: function(func) - { - if (func.__bridge_id__ == undefined) - { - func.__bridge_id__ = this.makeID(this.nextLocalFuncID++); - this.localFunctionCache[func.__bridge_id__] = func; - } - return func.__bridge_id__; - }, - - // serialization / deserialization - - serialize: function(value) - { - var result = {}; - - var t = typeof(value); - //primitives are kept as such - if (t == "number" || t == "string" || t == "boolean" || t == null || t == undefined) - { - result = value; - } - else if (value instanceof Array) - { - //arrays are serializesd recursively - result = []; - for (var i = 0; i < value.length; i++) - { - result[i] = this.serialize(value[i]); - } - } - else if (t == "function") - { - //js functions are assigned an ID and stored in the local cache - result.type = FABridge.TYPE_JSFUNCTION; - result.value = this.getFunctionID(value); - } - else if (value instanceof ASProxy) - { - result.type = FABridge.TYPE_ASINSTANCE; - result.value = value.fb_instance_id; - } - else - { - result.type = FABridge.TYPE_ANONYMOUS; - result.value = value; - } - - return result; - }, - - //on deserialization we always check the return for the specific error code that is used to marshall NPE's into JS errors - // the unpacking is done by returning the value on each pachet for objects/arrays - deserialize: function(packedValue) - { - - var result; - - var t = typeof(packedValue); - if (t == "number" || t == "string" || t == "boolean" || packedValue == null || packedValue == undefined) - { - result = this.handleError(packedValue); - } - else if (packedValue instanceof Array) - { - result = []; - for (var i = 0; i < packedValue.length; i++) - { - result[i] = this.deserialize(packedValue[i]); - } - } - else if (t == "object") - { - for(var i = 0; i < packedValue.newTypes.length; i++) - { - this.addTypeDataToCache(packedValue.newTypes[i]); - } - for (var aRefID in packedValue.newRefs) - { - this.createProxy(aRefID, packedValue.newRefs[aRefID]); - } - if (packedValue.type == FABridge.TYPE_PRIMITIVE) - { - result = packedValue.value; - } - else if (packedValue.type == FABridge.TYPE_ASFUNCTION) - { - result = this.getFunctionProxy(packedValue.value); - } - else if (packedValue.type == FABridge.TYPE_ASINSTANCE) - { - result = this.getProxy(packedValue.value); - } - else if (packedValue.type == FABridge.TYPE_ANONYMOUS) - { - result = packedValue.value; - } - } - return result; - }, - //increases the reference count for the given object - addRef: function(obj) - { - this.target.incRef(obj.fb_instance_id); - }, - //decrease the reference count for the given object and release it if needed - release:function(obj) - { - this.target.releaseRef(obj.fb_instance_id); - }, - - // check the given value for the components of the hard-coded error code : __FLASHERROR - // used to marshall NPE's into flash - - handleError: function(value) - { - if (typeof(value)=="string" && value.indexOf("__FLASHERROR")==0) - { - var myErrorMessage = value.split("||"); - if(FABridge.refCount > 0 ) - { - FABridge.refCount--; - } - throw new Error(myErrorMessage[1]); - return value; - } - else - { - return value; - } - } -}; - -// The root ASProxy class that facades a flash object - -ASProxy = function(bridge, typeName) -{ - this.bridge = bridge; - this.typeName = typeName; - return this; -}; -//methods available on each ASProxy object -ASProxy.prototype = -{ - get: function(propName) - { - return this.bridge.deserialize(this.bridge.getPropertyFromAS(this.fb_instance_id, propName)); - }, - - set: function(propName, value) - { - this.bridge.setPropertyInAS(this.fb_instance_id, propName, value); - }, - - call: function(funcName, args) - { - this.bridge.callASMethod(this.fb_instance_id, funcName, args); - }, - - addRef: function() { - this.bridge.addRef(this); - }, - - release: function() { - this.bridge.release(this); - } -}; +/* +/* +Copyright 2006 Adobe Systems Incorporated + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + + +/* + * The Bridge class, responsible for navigating AS instances + */ +function FABridge(target,bridgeName) +{ + this.target = target; + this.remoteTypeCache = {}; + this.remoteInstanceCache = {}; + this.remoteFunctionCache = {}; + this.localFunctionCache = {}; + this.bridgeID = FABridge.nextBridgeID++; + this.name = bridgeName; + this.nextLocalFuncID = 0; + FABridge.instances[this.name] = this; + FABridge.idMap[this.bridgeID] = this; + + return this; +} + +// type codes for packed values +FABridge.TYPE_ASINSTANCE = 1; +FABridge.TYPE_ASFUNCTION = 2; + +FABridge.TYPE_JSFUNCTION = 3; +FABridge.TYPE_ANONYMOUS = 4; + +FABridge.initCallbacks = {}; +FABridge.userTypes = {}; + +FABridge.addToUserTypes = function() +{ + for (var i = 0; i < arguments.length; i++) + { + FABridge.userTypes[arguments[i]] = { + 'typeName': arguments[i], + 'enriched': false + }; + } +} + +FABridge.argsToArray = function(args) +{ + var result = []; + for (var i = 0; i < args.length; i++) + { + result[i] = args[i]; + } + return result; +} + +function instanceFactory(objID) +{ + this.fb_instance_id = objID; + return this; +} + +function FABridge__invokeJSFunction(args) +{ + var funcID = args[0]; + var throughArgs = args.concat();//FABridge.argsToArray(arguments); + throughArgs.shift(); + + var bridge = FABridge.extractBridgeFromID(funcID); + return bridge.invokeLocalFunction(funcID, throughArgs); +} + +FABridge.addInitializationCallback = function(bridgeName, callback) +{ + var inst = FABridge.instances[bridgeName]; + if (inst != undefined) + { + callback.call(inst); + return; + } + + var callbackList = FABridge.initCallbacks[bridgeName]; + if(callbackList == null) + { + FABridge.initCallbacks[bridgeName] = callbackList = []; + } + + callbackList.push(callback); +} + +// updated for changes to SWFObject2 +function FABridge__bridgeInitialized(bridgeName) { + var objects = document.getElementsByTagName("object"); + var ol = objects.length; + var activeObjects = []; + if (ol > 0) { + for (var i = 0; i < ol; i++) { + if (typeof objects[i].SetVariable != "undefined") { + activeObjects[activeObjects.length] = objects[i]; + } + } + } + var embeds = document.getElementsByTagName("embed"); + var el = embeds.length; + var activeEmbeds = []; + if (el > 0) { + for (var j = 0; j < el; j++) { + if (typeof embeds[j].SetVariable != "undefined") { + activeEmbeds[activeEmbeds.length] = embeds[j]; + } + } + } + var aol = activeObjects.length; + var ael = activeEmbeds.length; + var searchStr = "bridgeName="+ bridgeName; + if ((aol == 1 && !ael) || (aol == 1 && ael == 1)) { + FABridge.attachBridge(activeObjects[0], bridgeName); + } + else if (ael == 1 && !aol) { + FABridge.attachBridge(activeEmbeds[0], bridgeName); + } + else { + var flash_found = false; + if (aol > 1) { + for (var k = 0; k < aol; k++) { + var params = activeObjects[k].childNodes; + for (var l = 0; l < params.length; l++) { + var param = params[l]; + if (param.nodeType == 1 && param.tagName.toLowerCase() == "param" && param["name"].toLowerCase() == "flashvars" && param["value"].indexOf(searchStr) >= 0) { + FABridge.attachBridge(activeObjects[k], bridgeName); + flash_found = true; + break; + } + } + if (flash_found) { + break; + } + } + } + if (!flash_found && ael > 1) { + for (var m = 0; m < ael; m++) { + var flashVars = activeEmbeds[m].attributes.getNamedItem("flashVars").nodeValue; + if (flashVars.indexOf(searchStr) >= 0) { + FABridge.attachBridge(activeEmbeds[m], bridgeName); + break; + } + } + } + } + return true; +} + +// used to track multiple bridge instances, since callbacks from AS are global across the page. + +FABridge.nextBridgeID = 0; +FABridge.instances = {}; +FABridge.idMap = {}; +FABridge.refCount = 0; + +FABridge.extractBridgeFromID = function(id) +{ + var bridgeID = (id >> 16); + return FABridge.idMap[bridgeID]; +} + +FABridge.attachBridge = function(instance, bridgeName) +{ + var newBridgeInstance = new FABridge(instance, bridgeName); + + FABridge[bridgeName] = newBridgeInstance; + +/* FABridge[bridgeName] = function() { + return newBridgeInstance.root(); + } +*/ + var callbacks = FABridge.initCallbacks[bridgeName]; + if (callbacks == null) + { + return; + } + for (var i = 0; i < callbacks.length; i++) + { + callbacks[i].call(newBridgeInstance); + } + delete FABridge.initCallbacks[bridgeName] +} + +// some methods can't be proxied. You can use the explicit get,set, and call methods if necessary. + +FABridge.blockedMethods = +{ + toString: true, + get: true, + set: true, + call: true +}; + +FABridge.prototype = +{ + + +// bootstrapping + + root: function() + { + return this.deserialize(this.target.getRoot()); + }, +//clears all of the AS objects in the cache maps + releaseASObjects: function() + { + return this.target.releaseASObjects(); + }, +//clears a specific object in AS from the type maps + releaseNamedASObject: function(value) + { + if(typeof(value) != "object") + { + return false; + } + else + { + var ret = this.target.releaseNamedASObject(value.fb_instance_id); + return ret; + } + }, +//create a new AS Object + create: function(className) + { + return this.deserialize(this.target.create(className)); + }, + + + // utilities + + makeID: function(token) + { + return (this.bridgeID << 16) + token; + }, + + + // low level access to the flash object + +//get a named property from an AS object + getPropertyFromAS: function(objRef, propName) + { + if (FABridge.refCount > 0) + { + throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); + } + else + { + FABridge.refCount++; + retVal = this.target.getPropFromAS(objRef, propName); + retVal = this.handleError(retVal); + FABridge.refCount--; + return retVal; + } + }, +//set a named property on an AS object + setPropertyInAS: function(objRef,propName, value) + { + if (FABridge.refCount > 0) + { + throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); + } + else + { + FABridge.refCount++; + retVal = this.target.setPropInAS(objRef,propName, this.serialize(value)); + retVal = this.handleError(retVal); + FABridge.refCount--; + return retVal; + } + }, + +//call an AS function + callASFunction: function(funcID, args) + { + if (FABridge.refCount > 0) + { + throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); + } + else + { + FABridge.refCount++; + retVal = this.target.invokeASFunction(funcID, this.serialize(args)); + retVal = this.handleError(retVal); + FABridge.refCount--; + return retVal; + } + }, +//call a method on an AS object + callASMethod: function(objID, funcName, args) + { + if (FABridge.refCount > 0) + { + throw new Error("You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround."); + } + else + { + FABridge.refCount++; + args = this.serialize(args); + retVal = this.target.invokeASMethod(objID, funcName, args); + retVal = this.handleError(retVal); + FABridge.refCount--; + return retVal; + } + }, + + // responders to remote calls from flash + + //callback from flash that executes a local JS function + //used mostly when setting js functions as callbacks on events + invokeLocalFunction: function(funcID, args) + { + var result; + var func = this.localFunctionCache[funcID]; + + if(func != undefined) + { + result = this.serialize(func.apply(null, this.deserialize(args))); + } + + return result; + }, + + // Object Types and Proxies + + // accepts an object reference, returns a type object matching the obj reference. + getTypeFromName: function(objTypeName) + { + return this.remoteTypeCache[objTypeName]; + }, + //create an AS proxy for the given object ID and type + createProxy: function(objID, typeName) + { + var objType = this.getTypeFromName(typeName); + instanceFactory.prototype = objType; + var instance = new instanceFactory(objID); + this.remoteInstanceCache[objID] = instance; + return instance; + }, + //return the proxy associated with the given object ID + getProxy: function(objID) + { + return this.remoteInstanceCache[objID]; + }, + + // accepts a type structure, returns a constructed type + addTypeDataToCache: function(typeData) + { + newType = new ASProxy(this, typeData.name); + var accessors = typeData.accessors; + for (var i = 0; i < accessors.length; i++) + { + this.addPropertyToType(newType, accessors[i]); + } + + var methods = typeData.methods; + for (var i = 0; i < methods.length; i++) + { + if (FABridge.blockedMethods[methods[i]] == undefined) + { + this.addMethodToType(newType, methods[i]); + } + } + + + this.remoteTypeCache[newType.typeName] = newType; + return newType; + }, + + //add a property to a typename; used to define the properties that can be called on an AS proxied object + addPropertyToType: function(ty, propName) + { + var c = propName.charAt(0); + var setterName; + var getterName; + if(c >= "a" && c <= "z") + { + getterName = "get" + c.toUpperCase() + propName.substr(1); + setterName = "set" + c.toUpperCase() + propName.substr(1); + } + else + { + getterName = "get" + propName; + setterName = "set" + propName; + } + ty[setterName] = function(val) + { + this.bridge.setPropertyInAS(this.fb_instance_id, propName, val); + } + ty[getterName] = function() + { + return this.bridge.deserialize(this.bridge.getPropertyFromAS(this.fb_instance_id, propName)); + } + }, + + //add a method to a typename; used to define the methods that can be callefd on an AS proxied object + addMethodToType: function(ty, methodName) + { + ty[methodName] = function() + { + return this.bridge.deserialize(this.bridge.callASMethod(this.fb_instance_id, methodName, FABridge.argsToArray(arguments))); + } + }, + + // Function Proxies + + //returns the AS proxy for the specified function ID + getFunctionProxy: function(funcID) + { + var bridge = this; + if (this.remoteFunctionCache[funcID] == null) + { + this.remoteFunctionCache[funcID] = function() + { + bridge.callASFunction(funcID, FABridge.argsToArray(arguments)); + } + } + return this.remoteFunctionCache[funcID]; + }, + + //reutrns the ID of the given function; if it doesnt exist it is created and added to the local cache + getFunctionID: function(func) + { + if (func.__bridge_id__ == undefined) + { + func.__bridge_id__ = this.makeID(this.nextLocalFuncID++); + this.localFunctionCache[func.__bridge_id__] = func; + } + return func.__bridge_id__; + }, + + // serialization / deserialization + + serialize: function(value) + { + var result = {}; + + var t = typeof(value); + //primitives are kept as such + if (t == "number" || t == "string" || t == "boolean" || t == null || t == undefined) + { + result = value; + } + else if (value instanceof Array) + { + //arrays are serializesd recursively + result = []; + for (var i = 0; i < value.length; i++) + { + result[i] = this.serialize(value[i]); + } + } + else if (t == "function") + { + //js functions are assigned an ID and stored in the local cache + result.type = FABridge.TYPE_JSFUNCTION; + result.value = this.getFunctionID(value); + } + else if (value instanceof ASProxy) + { + result.type = FABridge.TYPE_ASINSTANCE; + result.value = value.fb_instance_id; + } + else + { + result.type = FABridge.TYPE_ANONYMOUS; + result.value = value; + } + + return result; + }, + + //on deserialization we always check the return for the specific error code that is used to marshall NPE's into JS errors + // the unpacking is done by returning the value on each pachet for objects/arrays + deserialize: function(packedValue) + { + + var result; + + var t = typeof(packedValue); + if (t == "number" || t == "string" || t == "boolean" || packedValue == null || packedValue == undefined) + { + result = this.handleError(packedValue); + } + else if (packedValue instanceof Array) + { + result = []; + for (var i = 0; i < packedValue.length; i++) + { + result[i] = this.deserialize(packedValue[i]); + } + } + else if (t == "object") + { + for(var i = 0; i < packedValue.newTypes.length; i++) + { + this.addTypeDataToCache(packedValue.newTypes[i]); + } + for (var aRefID in packedValue.newRefs) + { + this.createProxy(aRefID, packedValue.newRefs[aRefID]); + } + if (packedValue.type == FABridge.TYPE_PRIMITIVE) + { + result = packedValue.value; + } + else if (packedValue.type == FABridge.TYPE_ASFUNCTION) + { + result = this.getFunctionProxy(packedValue.value); + } + else if (packedValue.type == FABridge.TYPE_ASINSTANCE) + { + result = this.getProxy(packedValue.value); + } + else if (packedValue.type == FABridge.TYPE_ANONYMOUS) + { + result = packedValue.value; + } + } + return result; + }, + //increases the reference count for the given object + addRef: function(obj) + { + this.target.incRef(obj.fb_instance_id); + }, + //decrease the reference count for the given object and release it if needed + release:function(obj) + { + this.target.releaseRef(obj.fb_instance_id); + }, + + // check the given value for the components of the hard-coded error code : __FLASHERROR + // used to marshall NPE's into flash + + handleError: function(value) + { + if (typeof(value)=="string" && value.indexOf("__FLASHERROR")==0) + { + var myErrorMessage = value.split("||"); + if(FABridge.refCount > 0 ) + { + FABridge.refCount--; + } + throw new Error(myErrorMessage[1]); + return value; + } + else + { + return value; + } + } +}; + +// The root ASProxy class that facades a flash object + +ASProxy = function(bridge, typeName) +{ + this.bridge = bridge; + this.typeName = typeName; + return this; +}; +//methods available on each ASProxy object +ASProxy.prototype = +{ + get: function(propName) + { + return this.bridge.deserialize(this.bridge.getPropertyFromAS(this.fb_instance_id, propName)); + }, + + set: function(propName, value) + { + this.bridge.setPropertyInAS(this.fb_instance_id, propName, value); + }, + + call: function(funcName, args) + { + this.bridge.callASMethod(this.fb_instance_id, funcName, args); + }, + + addRef: function() { + this.bridge.addRef(this); + }, + + release: function() { + this.bridge.release(this); + } +}; diff --git a/example/WebSocketMain.swf b/example/WebSocketMain.swf index e931770541ba070489b9cda66197f02150e0bcd2..8716475f9125679b481129e799be328a80f838c3 100644 GIT binary patch literal 180134 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 J0O25;?gJ@26(&Fh{&L2yeC8xvy3BwX_P6}V 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 literal 11780 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 m@+ZvO_!A~*j($mo_W0Aa0M*v + + + + + -


    +



    diff --git a/example/web_socket.js b/example/web_socket.js index 9bf6f1308..aaa80c1a1 100644 --- a/example/web_socket.js +++ b/example/web_socket.js @@ -1,164 +1,366 @@ -// Copyright: Hiroshi Ichikawa -// Lincense: New BSD Lincense -// Reference: http://dev.w3.org/html5/websockets/ -// Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-31 - -if (!window.WebSocket) { - - if (!window.console) console = {log: function(){ }, error: function(){ }}; - - WebSocket = function(url, protocol, proxyHost, proxyPort, headers) { - var self = this; - self.readyState = WebSocket.CONNECTING; - self.bufferedAmount = 0; - WebSocket.__addTask(function() { - self.__flash = - WebSocket.__flash.create(url, protocol, proxyHost || null, proxyPort || 0, headers || null); - - self.__flash.addEventListener("open", function(fe) { - try { - if (self.onopen) self.onopen(); - } catch (e) { - console.error(e.toString()); - } - }); - - self.__flash.addEventListener("close", function(fe) { - try { - if (self.onopen) self.onclose(); - } catch (e) { - console.error(e.toString()); - } - }); - - self.__flash.addEventListener("message", function(fe) { - var data = fe.getData(); - try { - if (self.onmessage) { - var e; - if (window.MessageEvent) { - e = document.createEvent("MessageEvent"); - e.initMessageEvent("message", false, false, data, null, null, window); - } else { // IE - e = {data: data}; - } - self.onmessage(e); - } - } catch (e) { - console.error(e.toString()); - } - }); - - self.__flash.addEventListener("stateChange", function(fe) { - try { - self.readyState = fe.getReadyState(); - self.bufferedAmount = fe.getBufferedAmount(); - } catch (e) { - console.error(e.toString()); - } - }); - - //console.log("[WebSocket] Flash object is ready"); - }); - } - - WebSocket.prototype.send = function(data) { - if (!this.__flash || this.readyState == WebSocket.CONNECTING) { - throw "INVALID_STATE_ERR: Web Socket connection has not been established"; - } - var result = this.__flash.send(data); - if (result < 0) { // success - return true; - } else { - this.bufferedAmount = result; - return false; - } - }; - - WebSocket.prototype.close = function() { - if (!this.__flash) return; - if (this.readyState != WebSocket.OPEN) return; - this.__flash.close(); - // Sets/calls them manually here because Flash WebSocketConnection.close cannot fire events - // which causes weird error: - // > You are trying to call recursively into the Flash Player which is not allowed. - this.readyState = WebSocket.CLOSED; - if (this.onclose) this.onclose(); - }; - - WebSocket.prototype.addEventListener = function() { - throw "Not implemented. Use e.g. onopen etc. instead." - }; - - WebSocket.CONNECTING = 0; - WebSocket.OPEN = 1; - WebSocket.CLOSED = 2; - - WebSocket.__tasks = []; - - WebSocket.__initialize = function() { - if (!WebSocket.__swfLocation) { - console.error("[WebSocket] set WebSocket.__swfLocation to location of WebSocketMain.swf"); - return; - } - var container = document.createElement("div"); - container.id = "webSocketContainer"; - // Puts the Flash out of the window. Note that we cannot use display: none or visibility: hidden - // here because it prevents Flash from loading at least in IE. - container.style.position = "absolute"; - container.style.left = "-100px"; - container.style.top = "-100px"; - var holder = document.createElement("div"); - holder.id = "webSocketFlash"; - container.appendChild(holder); - document.body.appendChild(container); - swfobject.embedSWF( - WebSocket.__swfLocation, "webSocketFlash", "10", "10", "9.0.0", - null, {bridgeName: "webSocket"}, null, null, - function(e) { - if (!e.success) console.error("[WebSocket] swfobject.embedSWF failed"); - } - ); - FABridge.addInitializationCallback("webSocket", function() { - try { - //console.log("[WebSocket] FABridge initializad"); - WebSocket.__flash = FABridge.webSocket.root(); - WebSocket.__flash.setCallerUrl(location.href); - for (var i = 0; i < WebSocket.__tasks.length; ++i) { - WebSocket.__tasks[i](); - } - WebSocket.__tasks = []; - } catch (e) { - console.error("[WebSocket] " + e.toString()); - } - }); - }; - - WebSocket.__addTask = function(task) { - if (WebSocket.__flash) { - task(); - } else { - WebSocket.__tasks.push(task); - } - } - - // called from Flash - function webSocketLog(message) { - console.log(message); - } - - // called from Flash - function webSocketError(message) { - console.error(message); - } - - if (window.addEventListener) { - window.addEventListener("load", WebSocket.__initialize, false); - } else { - window.attachEvent("onload", WebSocket.__initialize); - } - -} - -// Hard-coded, meh... -WebSocket.__swfLocation = "WebSocketMain.swf"; +// Copyright: Hiroshi Ichikawa +// License: New BSD License +// Reference: http://dev.w3.org/html5/websockets/ +// Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol + +(function() { + + if (window.WebSocket) return; + + var console = window.console; + if (!console) console = {log: function(){ }, error: function(){ }}; + + function hasFlash() { + if ('navigator' in window && 'plugins' in navigator && navigator.plugins['Shockwave Flash']) { + return !!navigator.plugins['Shockwave Flash'].description; + } + if ('ActiveXObject' in window) { + try { + return !!new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version'); + } catch (e) {} + } + return false; + } + + if (!hasFlash()) { + console.error("Flash Player is not installed."); + return; + } + console.log(location.protocol); + + WebSocket = function(url, protocol, proxyHost, proxyPort, headers) { + var self = this; + self.readyState = WebSocket.CONNECTING; + self.bufferedAmount = 0; + // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc. + // Otherwise, when onopen fires immediately, onopen is called before it is set. + setTimeout(function() { + WebSocket.__addTask(function() { + self.__createFlash(url, protocol, proxyHost, proxyPort, headers); + }); + }, 1); + } + + WebSocket.prototype.__createFlash = function(url, protocol, proxyHost, proxyPort, headers) { + var self = this; + self.__flash = + WebSocket.__flash.create(url, protocol, proxyHost || null, proxyPort || 0, headers || null); + + self.__flash.addEventListener("open", function(fe) { + try { + self.readyState = self.__flash.getReadyState(); + if (self.__timer) clearInterval(self.__timer); + if (window.opera) { + // Workaround for weird behavior of Opera which sometimes drops events. + self.__timer = setInterval(function () { + self.__handleMessages(); + }, 500); + } + if (self.onopen) self.onopen(); + } catch (e) { + console.error(e.toString()); + } + }); + + self.__flash.addEventListener("close", function(fe) { + try { + self.readyState = self.__flash.getReadyState(); + if (self.__timer) clearInterval(self.__timer); + if (self.onclose) self.onclose(); + } catch (e) { + console.error(e.toString()); + } + }); + + self.__flash.addEventListener("message", function() { + try { + self.__handleMessages(); + } catch (e) { + console.error(e.toString()); + } + }); + + self.__flash.addEventListener("error", function(fe) { + try { + if (self.__timer) clearInterval(self.__timer); + if (self.onerror) self.onerror(); + } catch (e) { + console.error(e.toString()); + } + }); + + self.__flash.addEventListener("stateChange", function(fe) { + try { + self.readyState = self.__flash.getReadyState(); + self.bufferedAmount = fe.getBufferedAmount(); + } catch (e) { + console.error(e.toString()); + } + }); + + //console.log("[WebSocket] Flash object is ready"); + }; + + WebSocket.prototype.send = function(data) { + if (this.__flash) { + this.readyState = this.__flash.getReadyState(); + } + if (!this.__flash || this.readyState == WebSocket.CONNECTING) { + throw "INVALID_STATE_ERR: Web Socket connection has not been established"; + } + var result = this.__flash.send(encodeURIComponent(data)); + if (result < 0) { // success + return true; + } else { + this.bufferedAmount = result; + return false; + } + }; + + WebSocket.prototype.close = function() { + if (!this.__flash) return; + this.readyState = this.__flash.getReadyState(); + if (this.readyState != WebSocket.OPEN) return; + this.__flash.close(); + // Sets/calls them manually here because Flash WebSocketConnection.close cannot fire events + // which causes weird error: + // > You are trying to call recursively into the Flash Player which is not allowed. + this.readyState = WebSocket.CLOSED; + if (this.__timer) clearInterval(this.__timer); + if (this.onclose) this.onclose(); + }; + + /** + * Implementation of {@link
    DOM 2 EventTarget Interface} + * + * @param {string} type + * @param {function} listener + * @param {boolean} useCapture !NB Not implemented yet + * @return void + */ + WebSocket.prototype.addEventListener = function(type, listener, useCapture) { + if (!('__events' in this)) { + this.__events = {}; + } + if (!(type in this.__events)) { + this.__events[type] = []; + if ('function' == typeof this['on' + type]) { + this.__events[type].defaultHandler = this['on' + type]; + this['on' + type] = this.__createEventHandler(this, type); + } + } + this.__events[type].push(listener); + }; + + /** + * Implementation of {@link DOM 2 EventTarget Interface} + * + * @param {string} type + * @param {function} listener + * @param {boolean} useCapture NB! Not implemented yet + * @return void + */ + WebSocket.prototype.removeEventListener = function(type, listener, useCapture) { + if (!('__events' in this)) { + this.__events = {}; + } + if (!(type in this.__events)) return; + for (var i = this.__events.length; i > -1; --i) { + if (listener === this.__events[type][i]) { + this.__events[type].splice(i, 1); + break; + } + } + }; + + /** + * Implementation of {@link DOM 2 EventTarget Interface} + * + * @param {WebSocketEvent} event + * @return void + */ + WebSocket.prototype.dispatchEvent = function(event) { + if (!('__events' in this)) throw 'UNSPECIFIED_EVENT_TYPE_ERR'; + if (!(event.type in this.__events)) throw 'UNSPECIFIED_EVENT_TYPE_ERR'; + + for (var i = 0, l = this.__events[event.type].length; i < l; ++ i) { + this.__events[event.type][i](event); + if (event.cancelBubble) break; + } + + if (false !== event.returnValue && + 'function' == typeof this.__events[event.type].defaultHandler) + { + this.__events[event.type].defaultHandler(event); + } + }; + + WebSocket.prototype.__handleMessages = function() { + // Gets data using readSocketData() instead of getting it from event object + // of Flash event. This is to make sure to keep message order. + // It seems sometimes Flash events don't arrive in the same order as they are sent. + var arr = this.__flash.readSocketData(); + for (var i = 0; i < arr.length; i++) { + var data = decodeURIComponent(arr[i]); + try { + if (this.onmessage) { + var e; + if (window.MessageEvent) { + e = document.createEvent("MessageEvent"); + e.initMessageEvent("message", false, false, data, null, null, window, null); + } else { // IE + e = {data: data}; + } + this.onmessage(e); + } + } catch (e) { + console.error(e.toString()); + } + } + }; + + /** + * @param {object} object + * @param {string} type + */ + WebSocket.prototype.__createEventHandler = function(object, type) { + return function(data) { + var event = new WebSocketEvent(); + event.initEvent(type, true, true); + event.target = event.currentTarget = object; + for (var key in data) { + event[key] = data[key]; + } + object.dispatchEvent(event, arguments); + }; + } + + /** + * Basic implementation of {@link DOM 2 EventInterface} + * + * @class + * @constructor + */ + function WebSocketEvent(){} + + /** + * + * @type boolean + */ + WebSocketEvent.prototype.cancelable = true; + + /** + * + * @type boolean + */ + WebSocketEvent.prototype.cancelBubble = false; + + /** + * + * @return void + */ + WebSocketEvent.prototype.preventDefault = function() { + if (this.cancelable) { + this.returnValue = false; + } + }; + + /** + * + * @return void + */ + WebSocketEvent.prototype.stopPropagation = function() { + this.cancelBubble = true; + }; + + /** + * + * @param {string} eventTypeArg + * @param {boolean} canBubbleArg + * @param {boolean} cancelableArg + * @return void + */ + WebSocketEvent.prototype.initEvent = function(eventTypeArg, canBubbleArg, cancelableArg) { + this.type = eventTypeArg; + this.cancelable = cancelableArg; + this.timeStamp = new Date(); + }; + + + WebSocket.CONNECTING = 0; + WebSocket.OPEN = 1; + WebSocket.CLOSING = 2; + WebSocket.CLOSED = 3; + + WebSocket.__tasks = []; + + WebSocket.__initialize = function() { + if (WebSocket.__swfLocation) { + // For backword compatibility. + window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation; + } + if (!window.WEB_SOCKET_SWF_LOCATION) { + console.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf"); + return; + } + var container = document.createElement("div"); + container.id = "webSocketContainer"; + // Puts the Flash out of the window. Note that we cannot use display: none or visibility: hidden + // here because it prevents Flash from loading at least in IE. + container.style.position = "absolute"; + container.style.left = "-100px"; + container.style.top = "-100px"; + var holder = document.createElement("div"); + holder.id = "webSocketFlash"; + container.appendChild(holder); + document.body.appendChild(container); + swfobject.embedSWF( + WEB_SOCKET_SWF_LOCATION, "webSocketFlash", "8", "8", "9.0.0", + null, {bridgeName: "webSocket"}, null, null, + function(e) { + if (!e.success) console.error("[WebSocket] swfobject.embedSWF failed"); + } + ); + FABridge.addInitializationCallback("webSocket", function() { + try { + //console.log("[WebSocket] FABridge initializad"); + WebSocket.__flash = FABridge.webSocket.root(); + WebSocket.__flash.setCallerUrl(location.href); + WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG); + for (var i = 0; i < WebSocket.__tasks.length; ++i) { + WebSocket.__tasks[i](); + } + WebSocket.__tasks = []; + } catch (e) { + console.error("[WebSocket] " + e.toString()); + } + }); + }; + + WebSocket.__addTask = function(task) { + if (WebSocket.__flash) { + task(); + } else { + WebSocket.__tasks.push(task); + } + } + + // called from Flash + window.webSocketLog = function(message) { + console.log(decodeURIComponent(message)); + }; + + // called from Flash + window.webSocketError = function(message) { + console.error(decodeURIComponent(message)); + }; + + if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) { + if (window.addEventListener) { + window.addEventListener("load", WebSocket.__initialize, false); + } else { + window.attachEvent("onload", WebSocket.__initialize); + } + } + +})(); diff --git a/src/WebSocket.java b/src/net/tootallnate/websocket/WebSocket.java similarity index 99% rename from src/WebSocket.java rename to src/net/tootallnate/websocket/WebSocket.java index f23aac429..ab3a26249 100644 --- a/src/WebSocket.java +++ b/src/net/tootallnate/websocket/WebSocket.java @@ -1,3 +1,4 @@ +package net.tootallnate.websocket; // TODO: Refactor into proper class hierarchy. import java.io.IOException; diff --git a/src/WebSocketClient.java b/src/net/tootallnate/websocket/WebSocketClient.java similarity index 96% rename from src/WebSocketClient.java rename to src/net/tootallnate/websocket/WebSocketClient.java index d21911e6c..d10c70de1 100644 --- a/src/WebSocketClient.java +++ b/src/net/tootallnate/websocket/WebSocketClient.java @@ -1,3 +1,4 @@ +package net.tootallnate.websocket; // TODO: Refactor into proper class hierarchy. import java.io.IOException; @@ -71,15 +72,28 @@ public WebSocketClient() { public WebSocketClient(URI serverUri) { setURI(serverUri); } + + /** + * Constructs a WebSocketClient instance and sets it to the connect to the + * specified URI. The client does not attempt to connect automatically. You + * must call connect first to initiate the socket connection. + * + * @param serverUri + */ + public WebSocketClient(URI serverUri, Draft draft) { + setURI(serverUri); + setDraft(draft); + } /** * Constructs a WebSocketClient instance and sets it to the connect to the - * specified URI using the specified subprotocol. The client does not attempt + * specified URI using the specified subprotocol and draft. The client does not attempt * to connect automatically. You must call connect first to * initiate the socket connection. * * @param serverUri * @param subprotocol + * @param draft */ public WebSocketClient(URI serverUri, String subprotocol) { setURI(serverUri); diff --git a/src/WebSocketHandshake.java b/src/net/tootallnate/websocket/WebSocketHandshake.java similarity index 99% rename from src/WebSocketHandshake.java rename to src/net/tootallnate/websocket/WebSocketHandshake.java index c8d45e7bc..62ef7d9d9 100644 --- a/src/WebSocketHandshake.java +++ b/src/net/tootallnate/websocket/WebSocketHandshake.java @@ -1,3 +1,4 @@ +package net.tootallnate.websocket; // TODO: Refactor into proper class hierarchy. import java.io.ByteArrayOutputStream; diff --git a/src/WebSocketListener.java b/src/net/tootallnate/websocket/WebSocketListener.java similarity index 98% rename from src/WebSocketListener.java rename to src/net/tootallnate/websocket/WebSocketListener.java index 84656fb9d..719fc240b 100644 --- a/src/WebSocketListener.java +++ b/src/net/tootallnate/websocket/WebSocketListener.java @@ -1,3 +1,4 @@ +package net.tootallnate.websocket; import java.io.IOException; import java.security.NoSuchAlgorithmException; diff --git a/src/WebSocketProtocol.java b/src/net/tootallnate/websocket/WebSocketProtocol.java similarity index 94% rename from src/WebSocketProtocol.java rename to src/net/tootallnate/websocket/WebSocketProtocol.java index 685d0cc54..62031cc6c 100644 --- a/src/WebSocketProtocol.java +++ b/src/net/tootallnate/websocket/WebSocketProtocol.java @@ -1,3 +1,4 @@ +package net.tootallnate.websocket; import java.nio.charset.Charset; /** diff --git a/src/WebSocketServer.java b/src/net/tootallnate/websocket/WebSocketServer.java similarity index 99% rename from src/WebSocketServer.java rename to src/net/tootallnate/websocket/WebSocketServer.java index 38eda63c4..98f42df16 100644 --- a/src/WebSocketServer.java +++ b/src/net/tootallnate/websocket/WebSocketServer.java @@ -1,3 +1,4 @@ +package net.tootallnate.websocket; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey;