// Copyright (c) 2014 Tom Zhou<iwebpp@gmail.com>
package com.iwebpp.wspp;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import android.util.Base64;
import com.iwebpp.libuvpp.handles.TimerHandle;
import com.iwebpp.node.EventEmitter2;
import com.iwebpp.node.NodeContext;
import com.iwebpp.node.NodeContext.TimeoutListener;
import com.iwebpp.node.NodeContext.nextTickListener;
import com.iwebpp.node.Url;
import com.iwebpp.node.http.Agent;
import com.iwebpp.node.http.ClientRequest;
import com.iwebpp.node.http.IncomingMessage;
import com.iwebpp.node.http.ReqOptions;
import com.iwebpp.node.http.http;
import com.iwebpp.node.http.httpp;
import com.iwebpp.node.net.AbstractSocket;
import com.iwebpp.node.stream.Readable2;
import com.iwebpp.node.stream.Writable.WriteCB;
import com.iwebpp.wspp.Receiver.opcOptions;
/**
* WebSocket implementation
*/
public class WebSocket
extends EventEmitter2 {
private static final String TAG = "WebSocket";
/**
* Constants
*/
// Default protocol version
public static final int ProtocolVersion = 13;
// Close timeout
public static final int CloseTimeout = 30000; // Allow 5 seconds to terminate the connection cleanly
/**
* Ready States
*/
/*["CONNECTING", "OPEN", "CLOSING", "CLOSED"].forEach(function (state, index) {
WebSocket.prototype[state] = WebSocket[state] = index;
});*/
public static final int CONNECTING = 0;
public static final int OPEN = 1;
public static final int CLOSING = 2;
public static final int CLOSED = 3;
public static class Options {
public String origin = null;
public int protocolVersion = WebSocket.ProtocolVersion;
public String protocolVersionHixie = null;
public String host = null;
public Map<String, String> headers = null;
public String protocol = null;
public Agent agent = null;
// ssl-related options
public String pfx = null;
public String key = null;
public String passphrase = null;
public String cert = null;
public String ca = null;
public String ciphers = null;
public boolean rejectUnauthorized = false;
// httpp-related options
///hole: {port: -1},
///httpp: false // default as HTTP not HTTPP
public boolean httpp = false;
public boolean https = false;
public int localPort = -1;
public String localAddress = null;
public Options() {
this.headers = new Hashtable<String, String>();
}
}
private AbstractSocket _socket;
private NodeContext context;
private int bytesReceived;
/**
* @return the bytesReceived
*/
public int getBytesReceived() {
return bytesReceived;
}
private int readyState;
private Map<String, Boolean> supports;
private int _closeCode;
private String _closeMessage;
private boolean _isServer;
private Sender _sender;
private Receiver _receiver;
private Listener cleanupWebsocketResources;
private List<Sendor> _queue = null;
private IncomingMessage upgradeReq;
/**
* @return the upgradeReq
*/
public IncomingMessage getUpgradeReq() {
return upgradeReq;
}
private String protocol;
/**
* @return the protocol
*/
public String getProtocol() {
return protocol;
}
/**
* @return the protocolVersion
*/
public int getProtocolVersion() {
return protocolVersion;
}
private int protocolVersion;
private String url;
/**
* @return the url
*/
public String getUrl() {
return url;
}
// Address info
public String remoteAddress() {
return this._socket.remoteAddress();
}
public int remotePort() {
return this._socket.remotePort();
}
public String localAddress() {
return this._socket.localAddress();
}
public int localPort() {
return this._socket.localPort();
}
private TimerHandle _closeTimer;
public WebSocket(NodeContext ctx, String address, List<String> protocols, Options options) throws Exception {
this.context = ctx;
final WebSocket self = this;
/*
if (protocols && !Array.isArray(protocols) && 'object' == typeof protocols) {
// accept the "options" Object as the 2nd argument
options = protocols;
protocols = null;
}
if ('string' == typeof protocols) {
protocols = [ protocols ];
}
if (!Array.isArray(protocols)) {
protocols = [];
}
// TODO: actually handle the `Sub-Protocols` part of the WebSocket client
this._socket = null;
this.bytesReceived = 0;
this.readyState = null;
this.supports = {};
if (Array.isArray(address)) {
initAsServerClient.apply(this, address.concat(options));
} else {
initAsClient.apply(this, [address, protocols, options]);
}
*/
this._socket = null;
this.bytesReceived = 0;
this.readyState = -1;/// null;
this.supports = new Hashtable<String, Boolean>();///{};
this._queue = null;
initAsClient(address, protocols, options);
this.cleanupWebsocketResources = new Listener(){
@Override
public void onEvent(Object error) throws Exception {
debug(TAG, "cleanupWebsocketResources:"+error!=null ? error.toString() : "");
if (self.readyState == WebSocket.CLOSED) return;
boolean emitClose = self.readyState != WebSocket.CONNECTING;
self.readyState = WebSocket.CLOSED;
// TBD...
///clearTimeout(this._closeTimer);
///self._closeTimer = null;
if (emitClose) self.emit("close", new close_code_b(
self._closeMessage!=null ? self._closeMessage : "",
self._closeCode>0 ? self._closeCode : 1000)); ///self.emit("close", self._closeCode || 1000, self._closeMessage || "");
if (self._socket!=null) {
self._socket.removeAllListeners();
// catch all socket error after removing all standard handlers
final AbstractSocket socket = self._socket;
/*self._socket.on("error", function() {
try { socket.destroy(); } catch (e) {}
});*/
self._socket.on("error", new Listener(){
public void onEvent(final Object data) throws Exception {
try { socket.destroy(null); } catch (Exception e) {}
}
});
try {
if (null==error) self._socket.end(null, null, null);
else self._socket.destroy(null);
}
catch (Exception e) { /* Ignore termination errors */ }
self._socket = null;
}
if (self._sender!=null) {
self._sender.removeAllListeners();
self._sender = null;
}
if (self._receiver!=null) {
self._receiver.cleanup();
self._receiver = null;
}
self.removeAllListeners();
self.on("error", new Listener(){
public void onEvent(final Object data) throws Exception {
}
});///function() {}); // catch all errors after this
/// TBD...
///delete this._queue;
}
};
}
protected WebSocket(NodeContext ctx, http.request_socket_head_b rsh, Options options) throws Exception {
this.context = ctx;
final WebSocket self = this;
/*
if (protocols && !Array.isArray(protocols) && 'object' == typeof protocols) {
// accept the "options" Object as the 2nd argument
options = protocols;
protocols = null;
}
if ('string' == typeof protocols) {
protocols = [ protocols ];
}
if (!Array.isArray(protocols)) {
protocols = [];
}
// TODO: actually handle the `Sub-Protocols` part of the WebSocket client
this._socket = null;
this.bytesReceived = 0;
this.readyState = null;
this.supports = {};
if (Array.isArray(address)) {
initAsServerClient.apply(this, address.concat(options));
} else {
initAsClient.apply(this, [address, protocols, options]);
}
*/
this._socket = null;
this.bytesReceived = 0;
this.readyState = -1;/// null;
this.supports = new Hashtable<String, Boolean>();///{};
this._queue = null;
initAsServerClient(rsh.getRequest(), rsh.getSocket(), rsh.getHead(), options);
this.cleanupWebsocketResources = new Listener(){
@Override
public void onEvent(Object error) throws Exception {
if (self.readyState == WebSocket.CLOSED) return;
boolean emitClose = self.readyState != WebSocket.CONNECTING;
self.readyState = WebSocket.CLOSED;
// TBD...
///clearTimeout(this._closeTimer);
///self._closeTimer = null;
if (emitClose) self.emit("close", new close_code_b(
self._closeMessage!=null ? self._closeMessage : "",
self._closeCode>0 ? self._closeCode : 1000)); ///self.emit("close", self._closeCode || 1000, self._closeMessage || "");
if (self._socket!=null) {
self._socket.removeAllListeners();
// catch all socket error after removing all standard handlers
final AbstractSocket socket = self._socket;
/*self._socket.on("error", function() {
try { socket.destroy(); } catch (e) {}
});*/
self._socket.on("error", new Listener(){
public void onEvent(final Object data) throws Exception {
try { socket.destroy(null); } catch (Exception e) {}
}
});
try {
if (null==error) self._socket.end(null, null, null);
else self._socket.destroy(null);
}
catch (Exception e) { /* Ignore termination errors */ }
self._socket = null;
}
if (self._sender!=null) {
self._sender.removeAllListeners();
self._sender = null;
}
if (self._receiver!=null) {
self._receiver.cleanup();
self._receiver = null;
}
self.removeAllListeners();
self.on("error", new Listener(){
public void onEvent(final Object data) throws Exception {
}
});///function() {}); // catch all errors after this
/// TBD...
///delete this._queue;
}
};
}
@SuppressWarnings("unused")
private WebSocket(){}
/**
* Gracefully closes the connection, after sending a description message to the server
*
* @param {Object} data to be sent to the server
* @throws Exception
* @api public
*/
public void close(int code, Object data) throws Exception {
if (this.readyState == WebSocket.CLOSING || this.readyState == WebSocket.CLOSED) return;
if (this.readyState == WebSocket.CONNECTING) {
this.readyState = WebSocket.CLOSED;
return;
}
try {
this.readyState = WebSocket.CLOSING;
this._closeCode = code;
this._closeMessage = data.toString();
boolean mask = !this._isServer;
this._sender.close(code, data, mask);
}
catch (Exception e) {
this.emit("error", e.toString());
}
finally {
this.terminate();
}
}
/**
* Pause the client stream
* @throws Exception
*
* @api public
*/
public void pause() throws Exception {
if (this.readyState != WebSocket.OPEN) throw new Exception("not opened");
this._socket.pause();
}
/**
* Sends a ping
*
* @param {Object} data to be sent to the server
* @param {Object} Members - mask: boolean, binary: boolean
* @param {boolean} dontFailWhenClosed indicates whether or not to throw if the connection isnt open
* @throws Exception
* @api public
*/
public void ping(Object data, SendOptions options, boolean dontFailWhenClosed) throws Exception {
if (this.readyState != WebSocket.OPEN) {
if (dontFailWhenClosed == true) return;
throw new Exception("not opened");
}
///options = options || {};
///if (typeof options.mask == 'undefined') options.mask = !this._isServer;
this._sender.ping(data, options);
}
/**
* Sends a pong
*
* @param {Object} data to be sent to the server
* @param {Object} Members - mask: boolean, binary: boolean
* @param {boolean} dontFailWhenClosed indicates whether or not to throw if the connection isnt open
* @throws Exception
* @api public
*/
public void pong(Object data, SendOptions options, boolean dontFailWhenClosed) throws Exception {
if (this.readyState != WebSocket.OPEN) {
if (dontFailWhenClosed == true) return;
throw new Exception("not opened");
}
///options = options!=null ? options : new SendOptions();
///if (typeof options.mask == 'undefined') options.mask = !this._isServer;
this._sender.pong(data, options);
}
/**
* Resume the client stream
* @throws Exception
*
* @api public
*/
public void resume() throws Exception {
if (this.readyState != WebSocket.OPEN) throw new Exception("not opened");
this._socket.resume();
}
/**
* Sends a piece of data
*
* @param {Object} data to be sent to the server
* @param {Object} Members - mask: boolean, binary: boolean
* @param {function} Optional callback which is executed after the send completes
* @throws Exception
* @api public
*/
public boolean send(final Object raw, final SendOptions options, final WriteCB cb) throws Exception {
/*if (typeof options == 'function') {
cb = options;
options = {};
}*/
if (this.readyState != WebSocket.OPEN) {
/*if (typeof cb == 'function') cb(new Error('not opened'));
else throw new Error('not opened');*/
if (cb != null) cb.writeDone("not opened");
else throw new Exception("not opened");
return false;
}
final Object data;
if (raw==null) {data = ""; options.binary = false;}
else data = raw;
if (this._queue != null) {
final WebSocket self = this;
///this._queue.push(function() { self.send(data, options, cb); });
this._queue.add(new Sendor(){
@Override
public void execute() throws Exception {
self.send(data, options, cb);
}
});
return false;
}
///options = options!=null ? options : new SendOptions();
options.fin = true;
/*
if (typeof options.binary == 'undefined') {
options.binary = (data instanceof ArrayBuffer || data instanceof Buffer ||
data instanceof Uint8Array ||
data instanceof Uint16Array ||
data instanceof Uint32Array ||
data instanceof Int8Array ||
data instanceof Int16Array ||
data instanceof Int32Array ||
data instanceof Float32Array ||
data instanceof Float64Array);
}*/
options.binary = data!=null ? data instanceof ByteBuffer : options.binary;
///if (typeof options.mask == 'undefined') options.mask = !this._isServer;
///var readable = typeof stream.Readable == 'function' ? stream.Readable : stream.Stream;
// TBD...
///if (data instanceof readable) {
if (data instanceof Readable2) {
startQueue();
sendStream(data, options, new WriteCB(){
public void writeDone(String error) throws Exception {
///process.nextTick(function() { executeQueueSends(self); });
///if (typeof cb == 'function') cb(error);
context.nextTick(new nextTickListener(){
@Override
public void onNextTick() throws Exception {
executeQueueSends();
}
});
if (cb != null) cb.writeDone(error);
}
});
return true;
}
else return this._sender.send(data, options, cb);
}
public static final class SendOptions {
public boolean binary = false;
public boolean mask = false;
protected boolean fin = false;
public SendOptions(boolean binary, boolean mask) {
this.binary = binary;
this.mask = mask;
}
}
private interface Sendor {
void execute() throws Exception;
}
private void startQueue() {
final WebSocket instance = this;
instance._queue = instance._queue!=null ? instance._queue : new LinkedList<Sendor>();
}
private void executeQueueSends() throws Exception {
final WebSocket instance = this;
List<Sendor> queue = instance._queue;
///if (typeof queue == 'undefined') return;
if (queue==null) return;
/*delete instance._queue;
for (var i = 0, l = queue.length; i < l; ++i) {
queue[i]();
}*/
instance._queue = null;
for (Sendor l : queue)
l.execute();
}
private void sendStream(Object data, final SendOptions options, final WriteCB cb) throws Exception {
final WebSocket instance = this;
/*stream.on('data', function(data) {
if (instance.readyState != WebSocket.OPEN) {
if (typeof cb == 'function') cb(new Error('not opened'));
else {
delete instance._queue;
instance.emit('error', new Error('not opened'));
}
return;
}
options.fin = false;
instance._sender.send(data, options);
});
stream.on('end', function() {
if (instance.readyState != WebSocket.OPEN) {
if (typeof cb == 'function') cb(new Error('not opened'));
else {
delete instance._queue;
instance.emit('error', new Error('not opened'));
}
return;
}
options.fin = true;
instance._sender.send(null, options);
if (typeof cb == 'function') cb(null);
});*/
Readable2 stream = (Readable2)data;
stream.on("data", new Listener(){
@Override
public void onEvent(Object data) throws Exception {
if (instance.readyState != WebSocket.OPEN) {
///if (typeof cb == 'function') cb(new Error('not opened'));
if (cb != null) cb.writeDone("not opened");
else {
///delete instance._queue;
instance._queue = null;
instance.emit("error", "not opened"/*new Error('not opened')*/);
}
return;
}
options.fin = false;
instance._sender.send(data, options, null);
}
});
stream.on("end", new Listener(){
@Override
public void onEvent(Object data) throws Exception {
if (instance.readyState != WebSocket.OPEN) {
///if (typeof cb == 'function') cb(new Error('not opened'));
if (cb != null) cb.writeDone("not opened");
else {
///delete instance._queue;
instance._queue = null;
instance.emit("error", "not opened"/*new Error('not opened')*/);
}
return;
}
options.fin = true;
instance._sender.send(null, options, null);
///if (typeof cb == 'function') cb(null);
if (cb != null) cb.writeDone(null);
}
});
}
/**
* Streams data through calls to a user supplied function
*
* @param {Object} Members - mask: boolean, binary: boolean
* @param {function} 'function (error, send)' which is executed on successive ticks of which send is 'function (data, final)'.
* @api public
*/
public interface StreamCallback {
public interface SendDone {
public void done(Object data, boolean finl) throws Exception;
}
// need to check isPrepand in case send == null
public void onStream(String error, SendDone send);
}
public static abstract class AbstractStreamCallback implements StreamCallback {
private SendDone done;
protected void setSendDone(SendDone send) {
this.done = send;
}
protected SendDone getSendDone() {
return this.done;
}
protected boolean isPrepand() {
return this.done != null;
}
protected AbstractStreamCallback(){
this.done = null;
}
}
// TBD...
public void stream(final SendOptions options, final AbstractStreamCallback cb) throws Exception {
/*if (typeof options == 'function') {
cb = options;
options = {};
}
*/
final WebSocket self = this;
if (cb == null /*typeof cb != 'function'*/) throw new Exception("callback must be provided");
if (this.readyState != WebSocket.OPEN) {
if (cb != null/*typeof cb == 'function'*/) cb.onStream("not opened", null);/// (new Error('not opened'));
else throw new Exception("not opened");
return;
}
if (this._queue != null) {
///this._queue.push(function() { self.stream(options, cb); });
this._queue.add(new Sendor(){
@Override
public void execute() throws Exception {
self.stream(options, cb);
}
});
return;
}
// TBD...
///options = options || {};
///if (typeof options.mask == 'undefined') options.mask = !this._isServer;
startQueue();
/*var send = function(data, final) {
try {
if (self.readyState != WebSocket.OPEN) throw new Error('not opened');
options.fin = final === true;
self._sender.send(data, options);
if (!final) process.nextTick(cb.bind(null, null, send));
else executeQueueSends(self);
}
catch (e) {
if (typeof cb == 'function') cb(e);
else {
delete self._queue;
self.emit('error', e);
}
}
}*/
///process.nextTick(cb.bind(null, null, send));
final StreamCallback.SendDone sendone = new StreamCallback.SendDone() {
@Override
public void done(Object data, boolean finl) throws Exception {
final StreamCallback.SendDone sself = this;
try {
if (self.readyState != WebSocket.OPEN) throw new Exception("not opened");
options.fin = finl == true;
self._sender.send(data, options, null);
///if (!finl) process.nextTick(cb.bind(null, null, send));
if (!finl) context.nextTick(new nextTickListener(){
@Override
public void onNextTick() throws Exception {
cb.setSendDone(sself);
}
});
else executeQueueSends();
}
catch (Exception e) {
///if (typeof cb == 'function') cb(e);
if (cb != null) cb.onStream(e.toString(), null);
else {
///delete self._queue;
self._queue.clear();
self.emit("error", e.toString());
}
}
}
};
context.nextTick(new nextTickListener(){
@Override
public void onNextTick() throws Exception {
cb.setSendDone(sendone);
}
});
}
/**
* Immediately shuts down the connection
* @throws Exception
*
* @api public
*/
public void terminate() throws Exception {
if (this.readyState == WebSocket.CLOSED) return;
if (this._socket != null) {
try {
// End the connection
this._socket.end(null, null, null);
}
catch (Exception e) {
// Socket error during end() call, so just destroy it right now
///cleanupWebsocketResources.call(this, true);
cleanupWebsocketResources.onEvent(null);
return;
}
// Add a timeout to ensure that the connection is completely
// cleaned up within 30 seconds, even if the clean close procedure
// fails for whatever reason
if (this._closeTimer != null) {
context.clearTimeout(this._closeTimer);
}
///this._closeTimer = setTimeout(cleanupWebsocketResources.bind(this, true), CloseTimeout);
this._closeTimer = context.setTimeout(new TimeoutListener(){
@Override
public void onTimeout() throws Exception {
cleanupWebsocketResources.onEvent(null);
}
}, CloseTimeout);
}
else if (this.readyState == WebSocket.CONNECTING) {
///cleanupWebsocketResources.call(this, true);
cleanupWebsocketResources.onEvent(null);
}
}
/**
* Expose bufferedAmount
*
* @api public
*/
public int bufferedAmount() {
int amount = 0;
if (this._socket!=null) {
amount = this._socket.bufferSize() > 0 ? this._socket.bufferSize() : 0;
}
return amount;
}
/**
* Emulates the W3C Browser based WebSocket interface using function members.
* @throws Exception
*
* @see http://dev.w3.org/html5/websockets/#the-websocket-interface
* @api public
*/
public void onopen(final onopenListener cb) throws Exception {
final WebSocket self = this;
this.removeListener("open");
this.on("open", new Listener(){
@Override
public void onEvent(Object data) throws Exception {
cb.onOpen(new OpenEvent(self));
}
});
}
public interface onopenListener {
public void onOpen(OpenEvent event) throws Exception;
}
public void onerror(final onerrorListener cb) throws Exception {
final WebSocket self = this;
this.removeListener("error");
this.on("error", new Listener(){
@Override
public void onEvent(Object raw) throws Exception {
if (raw!=null && raw instanceof error_code_b) {
error_code_b data = (error_code_b)raw;
cb.onError(new ErrorEvent(data.errorCode, data.reason, self));
} else {
cb.onError(new ErrorEvent(0, raw!=null? raw.toString() : "unknown", self));
}
}
});
}
public interface onerrorListener {
public void onError(ErrorEvent event) throws Exception;
}
public void onclose(final oncloseListener cb) throws Exception {
final WebSocket self = this;
this.removeListener("close");
this.on("close", new Listener(){
@Override
public void onEvent(Object raw) throws Exception {
if (raw != null && raw instanceof close_code_b) {
close_code_b data = (close_code_b)raw;
cb.onClose(new CloseEvent(data.closeCode, data.message, self));
} else {
cb.onClose(new CloseEvent(0, raw!=null ? raw.toString() : "unknown", self));
}
}
});
}
public interface oncloseListener {
public void onClose(CloseEvent event) throws Exception;
}
public void onmessage(final onmessageListener cb) throws Exception {
final WebSocket self = this;
this.removeListener("message");
this.on("message", new Listener(){
@Override
public void onEvent(Object raw) throws Exception {
message_data_b data = (message_data_b)raw;
cb.onMessage(new MessageEvent(data.data, data.flags.binary ? "Binary" : "Text", self));
}
});
}
public interface onmessageListener {
public void onMessage(MessageEvent event) throws Exception;
}
/**
* Emulates the W3C Browser based WebSocket interface using addEventListener.
*
* @see https://developer.mozilla.org/en/DOM/element.addEventListener
* @see http://dev.w3.org/html5/websockets/#the-websocket-interface
* @api public
*/
/*
WebSocket.prototype.addEventListener = function(method, listener) {
var target = this;
if (typeof listener === 'function') {
if (method === 'message') {
function onMessage (data, flags) {
listener.call(this, new MessageEvent(data, flags.binary ? 'Binary' : 'Text', target));
}
// store a reference so we can return the original function from the addEventListener hook
onMessage._listener = listener;
this.on(method, onMessage);
} else if (method === 'close') {
function onClose (code, message) {
listener.call(this, new CloseEvent(code, message, target));
}
// store a reference so we can return the original function from the addEventListener hook
onClose._listener = listener;
this.on(method, onClose);
} else if (method === 'error') {
function onError (event) {
event.target = target;
listener.call(this, event);
}
// store a reference so we can return the original function from the addEventListener hook
onError._listener = listener;
this.on(method, onError);
} else if (method === 'open') {
function onOpen () {
listener.call(this, new OpenEvent(target));
}
// store a reference so we can return the original function from the addEventListener hook
onOpen._listener = listener;
this.on(method, onOpen);
} else {
this.on(method, listener);
}
}
}
*/
/**
* W3C MessageEvent
*
* @see http://www.w3.org/TR/html5/comms.html
* @api private
*/
/*function MessageEvent(dataArg, typeArg, target) {
this.data = dataArg;
this.type = typeArg;
this.target = target;
}*/
public static final class MessageEvent {
/**
* @return the data
*/
public Object getData() {
return data;
}
/**
* @return the type
*/
public String getType() {
return type;
}
/**
* @return the type
*/
public boolean isBinary() {
return binary;
}
/**
* @return the target
*/
public WebSocket getTarget() {
return target;
}
private Object data;
private String type;
private boolean binary;
private WebSocket target;
public MessageEvent(Object data, String type, WebSocket target) {
this.data = data;
this.type = type;
this.target = target;
this.binary = "binary".equalsIgnoreCase(type);
}
@SuppressWarnings("unused")
private MessageEvent(){}
}
/**
* W3C CloseEvent
*
* @see http://www.w3.org/TR/html5/comms.html
* @api private
*/
/*function CloseEvent(code, reason, target) {
this.wasClean = (typeof code == 'undefined' || code == 1000);
this.code = code;
this.reason = reason;
this.target = target;
}*/
public static final class CloseEvent {
/**
* @return the code
*/
public int getCode() {
return code;
}
/**
* @return the reason
*/
public String getReason() {
return reason;
}
/**
* @return the target
*/
public WebSocket getTarget() {
return target;
}
/**
* @return the wasClean
*/
public boolean isWasClean() {
return wasClean;
}
private int code;
private String reason;
private WebSocket target;
private boolean wasClean;
public CloseEvent(int code, String reason, WebSocket target) {
this.code = code;
this.reason = reason;
this.target = target;
this.wasClean = (code < 0 || code == 1000);
}
@SuppressWarnings("unused")
private CloseEvent(){}
}
/**
* W3C OpenEvent
*
* @see http://www.w3.org/TR/html5/comms.html
* @api private
*/
/*function OpenEvent(target) {
this.target = target;
}*/
public static final class OpenEvent {
/**
* @return the target
*/
public WebSocket getTarget() {
return target;
}
private WebSocket target;
public OpenEvent(WebSocket target) {
this.target = target;
}
@SuppressWarnings("unused")
private OpenEvent(){}
}
public static final class ErrorEvent {
/**
* @return the error
*/
public int getCode() {
return code;
}
/**
* @return the error
*/
public String getError() {
return error;
}
/**
* @return the target
*/
public WebSocket getTarget() {
return target;
}
private int code;
private String error;
private WebSocket target;
public ErrorEvent(int code, String error, WebSocket target) {
this.code = code;
this.error = error;
this.target = target;
}
@SuppressWarnings("unused")
private ErrorEvent(){}
}
/**
* Entirely private apis,
* which may or may not be bound to a specific WebSocket instance.
* @throws Exception
*/
private void initAsServerClient(IncomingMessage req, AbstractSocket socket,
ByteBuffer upgradeHead, Options options) throws Exception {
/*options = new Options({
protocolVersion: protocolVersion,
protocol: null
}).merge(options);*/
///options.protocolVersion = WebSocket.ProtocolVersion;
///options.protocol = null;
// expose state properties
this.protocol = options.protocol;
this.protocolVersion = options.protocolVersion;
///this.supports.binary = (this.protocolVersion != 'hixie-76');
this.supports.put("binary", true); // always support binary
this.upgradeReq = req;
this.readyState = WebSocket.CONNECTING;
this._isServer = true;
// TBD...
// establish connection
///if (options.value.protocolVersion == 'hixie-76') establishConnection.call(this, ReceiverHixie, SenderHixie, socket, upgradeHead);
///else
///establishConnection.call(this, Receiver, Sender, socket, upgradeHead);
establishConnection(socket, upgradeHead);
}
private void initAsClient(String address, List<String> protocols, final Options options) throws Exception {
/*options = new Options({
origin: null,
protocolVersion: protocolVersion,
host: null,
headers: null,
protocol: null,
agent: null,
// ssl-related options
pfx: null,
key: null,
passphrase: null,
cert: null,
ca: null,
ciphers: null,
rejectUnauthorized: null,
// httpp-related options
hole: {port: -1},
httpp: false // default as HTTP not HTTPP
}).merge(options);*/
if (options.protocolVersion != 8 && options.protocolVersion != 13) {
throw new Exception("unsupported protocol version");
}
debug(TAG, "as client, options:"+options);
// verify url and establish http class
Url.UrlObj serverUrl = Url.parse(address);
boolean isUnixSocket = serverUrl.protocol == "ws+unix:";
if (null==serverUrl.host && !isUnixSocket) throw new Exception("invalid url");
boolean isSecure = serverUrl.protocol == "wss:" || serverUrl.protocol == "https:";
/// TBD...
///var httpObj = options.value.httpp ? (isSecure ? httpps : httpp) : (isSecure ? https : http);
int port = serverUrl.port>0 ? serverUrl.port : (isSecure ? 443 : 80);
String auth = serverUrl.auth;
// expose state properties
this._isServer = false;
this.url = address;
this.protocolVersion = options.protocolVersion;
///this.supports.binary = (this.protocolVersion != 'hixie-76');
this.supports.put("binary", true);
// TBD...
// begin handshake
/*var key = new Buffer(options.value.protocolVersion + '-' + Date.now()).toString('base64');
var shasum = crypto.createHash('sha1');
shasum.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
var expectedServerKey = shasum.digest('base64');
*/
String str = ""+options.protocolVersion+new Random(); ///""+options.protocolVersion+"-"+(new Date());
byte[] kbf = Base64.encode(str.getBytes(), Base64.DEFAULT);
String key = new String(kbf, "utf-8").trim();
MessageDigest shasum = MessageDigest.getInstance("SHA1");
byte[] sharet = shasum.digest((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes("utf-8"));
byte[] retbuf = Base64.encode(sharet, Base64.DEFAULT);
final String expectedServerKey = new String(retbuf, "utf-8").trim();
debug(TAG, "str:"+str+",srv key:"+key+",exp:"+expectedServerKey);
Agent agent = options.agent;
String headerHost = serverUrl.hostname;
// Append port number to Host and Origin header, only if specified in the url and non-default
if(serverUrl.port > 0) {
if((isSecure && (port != 443)) || (!isSecure && (port != 80))){
headerHost = headerHost + ':' + port;
}
}
/*
var requestOptions = {
port: port,
host: serverUrl.hostname,
hole: options.value.hole || {port: -1},
headers: {
'Connection': 'Upgrade',
'Upgrade': 'websocket',
'Host': headerHost,
'Origin': (isSecure ? 'https://' : 'http://') + headerHost,
'Sec-WebSocket-Version': options.value.protocolVersion,
'Sec-WebSocket-Key': key
}
};*/
ReqOptions requestOptions = new ReqOptions();
requestOptions.method = "GET";
requestOptions.port = port;
requestOptions.hostname = serverUrl.hostname;
requestOptions.localPort = options.localPort;
requestOptions.localAddress = options.localAddress;
requestOptions.headers.put("Connection", new ArrayList<String>());
requestOptions.headers.get("Connection").add("Upgrade");
requestOptions.headers.put("Upgrade", new ArrayList<String>());
requestOptions.headers.get("Upgrade").add("websocket");
requestOptions.headers.put("Host", new ArrayList<String>());
requestOptions.headers.get("Host").add(headerHost);
requestOptions.headers.put("Origin", new ArrayList<String>());
requestOptions.headers.get("Origin").add((isSecure ? "https://" : "http://") + headerHost);
requestOptions.headers.put("Sec-WebSocket-Version", new ArrayList<String>());
requestOptions.headers.get("Sec-WebSocket-Version").add(""+options.protocolVersion);
requestOptions.headers.put("Sec-WebSocket-Key", new ArrayList<String>());
requestOptions.headers.get("Sec-WebSocket-Key").add(key);
// If we have basic auth.
if (auth!=null) {
///requestOptions.headers['Authorization'] = 'Basic ' + new Buffer(auth).toString('base64');
byte[] authbuf = Base64.encode(auth.trim().getBytes(), Base64.DEFAULT);
String authstr = new String(authbuf, "utf-8").trim();
requestOptions.headers.put("Authorization", new ArrayList<String>());
requestOptions.headers.get("Authorization").add("Basic " + authstr);
}
if (options.protocol != null) {
///requestOptions.headers['Sec-WebSocket-Protocol'] = options.value.protocol;
requestOptions.headers.put("Sec-WebSocket-Protocol", new ArrayList<String>());
requestOptions.headers.get("Sec-WebSocket-Protocol").add(options.protocol);
}
if (options.host != null) {
///requestOptions.headers['Host'] = options.value.host;
requestOptions.headers.put("Host", new ArrayList<String>());
requestOptions.headers.get("Host").add(options.host);
}
if (options.headers != null && !options.headers.isEmpty()) {
/*for (var header in options.value.headers) {
if (options.value.headers.hasOwnProperty(header)) {
requestOptions.headers[header] = options.value.headers[header];
}
}*/
for (Entry<String, String> kv : options.headers.entrySet()) {
requestOptions.headers.put(kv.getKey(), new ArrayList<String>());
requestOptions.headers.get(kv.getKey()).add(kv.getValue());
}
}
/*
if (options.isDefinedAndNonNull('pfx')
|| options.isDefinedAndNonNull('key')
|| options.isDefinedAndNonNull('passphrase')
|| options.isDefinedAndNonNull('cert')
|| options.isDefinedAndNonNull('ca')
|| options.isDefinedAndNonNull('ciphers')
|| options.isDefinedAndNonNull('rejectUnauthorized')) {
if (options.isDefinedAndNonNull('pfx')) requestOptions.pfx = options.value.pfx;
if (options.isDefinedAndNonNull('key')) requestOptions.key = options.value.key;
if (options.isDefinedAndNonNull('passphrase')) requestOptions.passphrase = options.value.passphrase;
if (options.isDefinedAndNonNull('cert')) requestOptions.cert = options.value.cert;
if (options.isDefinedAndNonNull('ca')) requestOptions.ca = options.value.ca;
if (options.isDefinedAndNonNull('ciphers')) requestOptions.ciphers = options.value.ciphers;
if (options.isDefinedAndNonNull('rejectUnauthorized')) requestOptions.rejectUnauthorized = options.value.rejectUnauthorized;
if (!agent) {
// global agent ignores client side certificates
agent = new httpObj.Agent(requestOptions);
}
}
*/
debug(TAG, "serverUrl.path:"+serverUrl.path);
requestOptions.path = serverUrl.path!=null && serverUrl.path!="" ? serverUrl.path : "/";
if (agent!=null) {
requestOptions.agent = agent;
}
if (isUnixSocket) {
requestOptions.socketPath = serverUrl.pathname;
}
if (options.origin != null) {
if (options.protocolVersion < 13) {
requestOptions.headers.put("Sec-WebSocket-Origin", new ArrayList<String>());
requestOptions.headers.get("Sec-WebSocket-Origin").add(options.origin);
} else {
requestOptions.headers.put("Origin", new ArrayList<String>());
requestOptions.headers.get("Origin").add(options.origin);
}
}
final WebSocket self = this;
// TBD... https, httpps
///var req = httpObj.request(requestOptions);
final ClientRequest req = options.httpp ?
httpp.request(context, requestOptions, null) :
http.request(context, requestOptions, null);
/*req.on('error', function(error) {
self.emit('error', error);
cleanupWebsocketResources.call(this, error);
});*/
req.on("error", new Listener(){
@Override
public void onEvent(Object error) throws Exception {
self.emit("error", error!=null? error.toString() : null);
cleanupWebsocketResources.onEvent(error!=null? error.toString() : null);
}
});
/*
req.once('response', function(res) {
if (!self.emit('unexpected-response', req, res)) {
var error = new Error('unexpected server response (' + res.statusCode + ')');
req.abort();
self.emit('error', error);
}
cleanupWebsocketResources.call(this, error);
});
*/
req.onceResponse(new ClientRequest.responseListener(){
public void onResponse(IncomingMessage res) throws Exception {
String error=null;
if (!self.emit("unexpected-response", /*req,*/ res)) {
error = "unexpected server response (" + res.statusCode() + ")"; ///new Error('unexpected server response (' + res.statusCode + ')');
req.abort();
self.emit("error", error);
}
cleanupWebsocketResources.onEvent(error);
}
});
req.onceUpgrade(new ClientRequest.upgradeListener(){
@Override
public void onUpgrade(IncomingMessage res, AbstractSocket socket,
ByteBuffer upgradeHead) throws Exception {
debug(TAG, "got upgrade");
if (self.readyState == WebSocket.CLOSED) {
// client closed before server accepted connection
self.emit("close");
self.removeAllListeners();
socket.end(null, null, null);
return;
}
debug(TAG, "res.headers:"+res.headers());
String serverKey =
res.headers().containsKey("sec-websocket-accept") ?
(res.headers().get("sec-websocket-accept").isEmpty() ? null :
res.headers().get("sec-websocket-accept").get(0)) : null; ///res.headers['sec-websocket-accept'];
// TBD...
///if (typeof serverKey == 'undefined' || serverKey !== expectedServerKey) {
if (serverKey == null || !serverKey.trim().equals(expectedServerKey)) {
debug(TAG, "invalid server key:"+serverKey+", expectedServerKey:"+expectedServerKey);
self.emit("error", "invalid server key");
self.removeAllListeners();
socket.end(null, null, null);
return;
}
String serverProt =
res.headers().containsKey("sec-websocket-protocol") ?
(res.headers().get("sec-websocket-protocol").isEmpty() ? null :
res.headers().get("sec-websocket-protocol").get(0)) : null;
///String[] protList = (options.protocol!=null ? options.protocol : "").split(", *"); ///(options.value.protocol || "").split(/, */);
String protList = (options.protocol!=null ? options.protocol : "");
String protError = null;
if (null==options.protocol && serverProt!=null) {
protError = "server sent a subprotocol even though none requested";
} else if (null!=options.protocol && null==serverProt) {
protError = "server sent no subprotocol even though requested";
} else if (serverProt!=null && (protList.indexOf(serverProt) == -1)) {
protError = "server responded with an invalid protocol";
}
if (protError!=null) {
self.emit("error", protError);
self.removeAllListeners();
socket.end(null, null, null);
return;
} else if (serverProt != null) {
self.protocol = serverProt;
}
establishConnection(/*self, Receiver, Sender,*/ socket, upgradeHead);
// perform cleanup on http resources
req.removeAllListeners();
// TBD...
///req = null;
///agent = null;
}
});
req.end(null, null, null);
this.readyState = WebSocket.CONNECTING;
}
// POJO beans
protected static class message_data_b {
public Object data;
public opcOptions flags;
public message_data_b(Object data, opcOptions flags) {
this.data = data;
this.flags = flags;
}
}
protected static class error_code_b {
public int errorCode;
public String reason;
public error_code_b(String reason, int errorCode) {
this.reason = reason;
this.errorCode = errorCode;
}
}
protected static class close_code_b {
public int closeCode;
public String message;
public close_code_b(String message, int closeCode) {
this.message = message;
this.closeCode = closeCode;
}
}
// receiver event handlers
private class ReceiverClass extends Receiver {
protected ReceiverClass() throws Exception {
super();
// TODO Auto-generated constructor stub
}
@Override
protected void ontext(String text, opcOptions options) throws Exception {
options.binary = false;
emit("message", new message_data_b(text, options));
}
@Override
protected void onbinary(ByteBuffer buf, opcOptions options) throws Exception {
options.binary = true;
emit("message", new message_data_b(buf, options));
}
@Override
protected void onping(ByteBuffer buf, opcOptions options) throws Exception {
///flags = flags || {};
///pong(data, {mask: !self._isServer, binary: flags.binary === true}, true);
pong(buf, new SendOptions(options.binary, !_isServer), true);
emit("ping", new message_data_b(buf, options));
}
@Override
protected void onpong(ByteBuffer buf, opcOptions options) throws Exception {
emit("pong", new message_data_b(buf, options));
}
@Override
protected void onclose(int code, String message, opcOptions options) throws Exception {
///flags = flags || {};
close(code, message);
}
@Override
protected void onerror(String reason, int errorCode) throws Exception {
// close the connection when the receiver reports a HyBi error code
/*self.close(typeof errorCode != 'undefined' ? errorCode : 1002, "");
if (self.listeners("error").length > 0) {
self.emit('error', reason, errorCode);
}*/
close(errorCode != 0 ? errorCode : 1002, "");
if (listenerCount("error") > 0) {
emit("error", new error_code_b(reason, errorCode));
}
}
}
private void establishConnection(
/*Receiver ReceiverClass, Sender SenderClass, */
final AbstractSocket socket, final ByteBuffer upgradeHead) throws Exception {
this._socket = socket;
///socket.setTimeout(0);
socket.setNoDelay(true);
final WebSocket self = this;
this._receiver = new ReceiverClass();
// socket cleanup handlers
/*socket.on('end', cleanupWebsocketResources.bind(this));
socket.on('close', cleanupWebsocketResources.bind(this));
socket.on('error', cleanupWebsocketResources.bind(this));
*/
socket.on("end", cleanupWebsocketResources);
socket.on("close", cleanupWebsocketResources);
socket.on("error", cleanupWebsocketResources);
// ensure that the upgradeHead is added to the receiver
/*function firstHandler(data) {
if (self.readyState != WebSocket.OPEN) return;
if (upgradeHead && upgradeHead.length > 0) {
self.bytesReceived += upgradeHead.length;
var head = upgradeHead;
upgradeHead = null;
self._receiver.add(head);
}
dataHandler = realHandler;
if (data) {
self.bytesReceived += data.length;
self._receiver.add(data);
}
}*/
// subsequent packets are pushed straight to the receiver
/*function realHandler(data) {
if (data) self.bytesReceived += data.length;
self._receiver.add(data);
}*/
final Listener realHandler = new Listener(){
@Override
public void onEvent(Object raw) throws Exception {
ByteBuffer data = (ByteBuffer)raw;
debug(TAG, "realHandler: "+data);
if (data!=null) self.bytesReceived += data.capacity();
self._receiver.add(data);
}
};
///var dataHandler = firstHandler;
final Listener firstHandler = new Listener(){
@Override
public void onEvent(Object raw) throws Exception {
ByteBuffer data = (ByteBuffer)raw;
debug(TAG, "firstHandler, data: "+data+", upgradeHead:"+upgradeHead);
// TBD...
///dataHandler = realHandler;
socket.removeListener("data", this);
socket.addListener("data", realHandler);
///socket.on("data", realHandler);
debug(TAG, "retrain data handler");
if (self.readyState != WebSocket.OPEN) return;
if (upgradeHead!=null && upgradeHead.capacity() > 0) {
self.bytesReceived += upgradeHead.capacity();
// copy one
ByteBuffer head = ByteBuffer.allocate(upgradeHead.capacity());
head.put(upgradeHead); head.flip();
// TBD...
///upgradeHead = null;
upgradeHead.clear();
self._receiver.add(head);
}
if (data != null) {
self.bytesReceived += data.capacity();
self._receiver.add(data);
}
}
};
// if data was passed along with the http upgrade,
// this will schedule a push of that on to the receiver.
// this has to be done on next tick, since the caller
// hasn't had a chance to set event handlers on this client
// object yet.
///process.nextTick(firstHandler);
context.nextTick(new nextTickListener(){
@Override
public void onNextTick() throws Exception {
firstHandler.onEvent(null);
}
});
/*
// receiver event handlers
self._receiver.ontext = function (data, flags) {
flags = flags || {};
self.emit('message', data, flags);
};
self._receiver.onbinary = function (data, flags) {
flags = flags || {};
flags.binary = true;
self.emit('message', data, flags);
};
self._receiver.onping = function(data, flags) {
flags = flags || {};
self.pong(data, {mask: !self._isServer, binary: flags.binary === true}, true);
self.emit('ping', data, flags);
};
self._receiver.onpong = function(data, flags) {
self.emit('pong', data, flags);
};
self._receiver.onclose = function(code, data, flags) {
flags = flags || {};
self.close(code, data);
};
self._receiver.onerror = function(reason, errorCode) {
// close the connection when the receiver reports a HyBi error code
self.close(typeof errorCode != 'undefined' ? errorCode : 1002, '');
if (self.listeners('error').length > 0) {
self.emit('error', reason, errorCode);
}
};
*/
// finalize the client
this._sender = new Sender(socket);
/*this._sender.on('error', function(error) {
self.close(1002, '');
self.emit('error', error);
});*/
this._sender.on("error", new Listener(){
public void onEvent(final Object error) throws Exception {
self.close(1002, "");
self.emit("error", error!=null ? error.toString() : null);
}
});
this.readyState = WebSocket.OPEN;
this.emit("open");
// TBD...
///socket.on("data", dataHandler);
socket.on("data", firstHandler);
socket.on("drain", new Listener(){
public void onEvent(final Object error) throws Exception {
self.emit("drain");
}
});
}
}