From d90a8a7fc0cc42d825c17fc3992a5c7928eeedfd Mon Sep 17 00:00:00 2001 From: Shuo Li Date: Fri, 29 Aug 2014 12:29:56 +0800 Subject: [PATCH 01/30] Fix the cfg_addr error on socket object --- socketio/server.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/socketio/server.py b/socketio/server.py index 74f2428..eada8ee 100644 --- a/socketio/server.py +++ b/socketio/server.py @@ -69,7 +69,11 @@ def __init__(self, *args, **kwargs): try: address = args[0].address[0] except AttributeError: - address = args[0].cfg_addr[0] + try: + address = args[0].cfg_addr[0] + except AttributeError: + address = args[0].getsockname()[0] + policylistener = kwargs.pop('policy_listener', (address, 10843)) self.policy_server = FlashPolicyServer(policylistener) else: From ae3a8407b192330edc38d137edcc836a93596b9e Mon Sep 17 00:00:00 2001 From: Shuo Li Date: Fri, 5 Sep 2014 12:00:18 +0800 Subject: [PATCH 02/30] Save current work. Now the polling can connect but flooding the service --- setup.py | 2 +- socketio/engine/__init__.py | 1 + socketio/engine/handler.py | 138 + socketio/engine/parser.py | 231 + socketio/engine/requests.py | 12 + socketio/engine/socket.py | 434 ++ socketio/engine/tests/__init__.py | 1 + socketio/engine/tests/test_parser.py | 111 + socketio/engine/transports.py | 345 + socketio/handler.py | 222 +- socketio/server.py | 21 +- socketio/transports.py | 317 - socketio/virtsocket.py | 479 -- tests/jstests/jstests.py | 17 +- tests/jstests/static/socket.io.js | 8700 ++++++++++++++++---------- tests/jstests/tests/suite.js | 73 +- tests/test_namespace.py | 5 +- tests/test_socket.py | 2 +- 18 files changed, 6806 insertions(+), 4305 deletions(-) create mode 100644 socketio/engine/__init__.py create mode 100644 socketio/engine/handler.py create mode 100644 socketio/engine/parser.py create mode 100644 socketio/engine/requests.py create mode 100644 socketio/engine/socket.py create mode 100644 socketio/engine/tests/__init__.py create mode 100644 socketio/engine/tests/test_parser.py create mode 100644 socketio/engine/transports.py delete mode 100644 socketio/transports.py delete mode 100644 socketio/virtsocket.py diff --git a/setup.py b/setup.py index 9670ccd..d772428 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def run_tests(self): install_requires=("gevent", "gevent-websocket",), setup_requires=('versiontools >= 1.7'), cmdclass = {'test': PyTest}, - tests_require=['pytest', 'mock'], + tests_require=['pytest', 'mock', 'gevent', 'pyee'], packages=find_packages(exclude=["examples", "tests"]), classifiers=[ "Development Status :: 4 - Beta", diff --git a/socketio/engine/__init__.py b/socketio/engine/__init__.py new file mode 100644 index 0000000..50f2cf9 --- /dev/null +++ b/socketio/engine/__init__.py @@ -0,0 +1 @@ +__author__ = 'lishuo' diff --git a/socketio/engine/handler.py b/socketio/engine/handler.py new file mode 100644 index 0000000..a455e05 --- /dev/null +++ b/socketio/engine/handler.py @@ -0,0 +1,138 @@ +# coding=utf-8 +""" +The wsgi handler for Engine, it accepts requests for engine protocol +""" +from __future__ import absolute_import + +import copy +import urlparse +import weakref +import gevent +from gevent.pywsgi import WSGIHandler +import sys +from pyee import EventEmitter +from webob import Request, Response +from . import transports +from .socket import Socket + + +class EngineHandler(WSGIHandler, EventEmitter): + clients = {} + + handler_types = { + 'websocket': transports.WebsocketTransport, + 'flashsocket': transports.FlashSocketTransport, + 'xhr-polling': transports.XHRPollingTransport, + 'polling': transports.XHRPollingTransport, + 'jsonp-polling': transports.JSONPollingTransport, + } + + def __init__(self, config, *args, **kwargs): + """Create a new SocketIOHandler. + + :param config: dict Configuration for timeouts and intervals + that will go down to the other components, transports, etc.. + + """ + self.config = config + self.request = None + self.response = None + + super(EngineHandler, self).__init__(*args, **kwargs) + EventEmitter.__init__(self) + + self.transports = self.handler_types.keys() + + if self.server.transports: + self.transports = self.server.transports + if not set(self.transports).issubset(set(self.handler_types)): + raise ValueError("transports should be elements of: %s" % + (self.handler_types.keys())) + + self.out_headers = {} + + def handle_one_response(self): + try: + path = self.environ.get('PATH_INFO') + + if not path.lstrip('/').startswith(self.server.resource + '/'): + return super(EngineHandler, self).handle_one_response() + + # Create a request and a response and attach the handler instance to each of them + self.request = Request(self.get_environ()) + setattr(self.request, 'handler', weakref.ref(self)) + + self.response = Response() + setattr(self.response, 'handler', weakref.ref(self)) + + qs_dict = self.request.GET + + transport = qs_dict.get("transport", None) + sid = qs_dict.get("sid", None) + b64 = qs_dict.get("b64", False) + + socket = self.clients.get(sid, None) + + if socket is None: + self._do_handshake(transport_name=transport, b64=b64) + + self.application = self.response + self.close_connection = True + super(EngineHandler, self).handle_one_response() + return + + if 'Upgrade' in self.request.headers: + upgrade = self.request.headers['Upgrade'] + raise NotImplementedError() + else: + socket.transport.on_handler(self) + + # Check the response which should be filled already, set it as the application callable and do cleanup + try: + self.close_connection = True + self.application = self.response + super(EngineHandler, self).handle_one_response() + return + finally: + # Clean up circular references so they can be garbage collected. + if hasattr(self, 'websocket') and self.websocket: + if hasattr(self.websocket, 'environ'): + del self.websocket.environ + del self.websocket + if self.environ: + del self.environ + finally: + self.emit("cleanup") + + def _do_handshake(self, transport_name, b64=False): + if transport_name not in self.handler_types: + raise ValueError("transport name [%s] not supported" % transport_name) + + options = copy.copy(self.config) + options['supports_binary'] = not b64 + + transport_class = self.handler_types[transport_name] + transport = transport_class(self, options) + transport.on_handler(self) + + socket = Socket(transport) + self.clients[socket.sessid] = socket + + self.out_headers['Set-Cookie'] = 'io=%s' % socket.sessid + socket.on_open() + + def write_jsonp_result(self, data, wrapper="0"): + self.start_response("200 OK", [ + ("Content-Type", "application/javascript"), + ]) + self.result = ['io.j[%s]("%s");' % (wrapper, data)] + + def write_plain_result(self, data): + self.start_response("200 OK", [ + ("Access-Control-Allow-Origin", self.environ.get('HTTP_ORIGIN', '*')), + ("Access-Control-Allow-Credentials", "true"), + ("Access-Control-Allow-Methods", "POST, GET, OPTIONS"), + ("Access-Control-Max-Age", 3600), + ("Content-Type", "text/plain"), + ]) + self.result = [data] diff --git a/socketio/engine/parser.py b/socketio/engine/parser.py new file mode 100644 index 0000000..950b301 --- /dev/null +++ b/socketio/engine/parser.py @@ -0,0 +1,231 @@ +# coding=utf-8 +import base64 + +empty_byte_array = bytearray() + + +class Parser(object): + """ + The parser which encode and decode engine packet + """ + + # Current protocol version + protocol = 3 + + # Packet type + packet_types = { + "open": 0, + "close": 1, + "ping": 2, + "pong": 3, + "message": 4, + "upgrade": 5, + "noop": 6, + } + + packet_type_lists = ( + "open", + "close", + "ping", + "pong", + "message", + "upgrade", + "noop" + ) + + # Parser error packet + error_packet = { + "type": "error", + "data": "parser error" + } + + @staticmethod + def encode_packet(packet, supports_binary=True, utf8_encoding=True): + data = packet.get("data", None) + + type_buffer = str(Parser.packet_types[packet['type']]) + + if data: + if type(data) == bytearray: + if not supports_binary: + return Parser.encode_base64_packet(packet) + + return type_buffer + data + + # Now we have a string or something, convert it to string first + data = str(data) + if utf8_encoding: + data = data.encode("utf-8") + + return str(type_buffer) + data + else: + return str(type_buffer) + + @staticmethod + def encode_base64_packet(packet): + """ + Encode the packet to a base64 string + :param packet: + :return: The base64 string + """ + data = packet["data"] + if hasattr(data, "buffer"): + data = data.buffer + + return 'b' + str(Parser.packet_types[packet["type"]]) + base64.standard_b64encode(data) + + @staticmethod + def decode_packet(data, utf8_decode=False): + if type(data) == str: + if data[0] == 'b': + return Parser.decode_base64_packet(data[1:]) + + packet_type = data[0] + + if utf8_decode: + # TODO catch and throw an customized exception? Or not + data = data.decode('utf-8') + + packet_type = Parser.packet_type_lists[bytearray(packet_type)[0]] + + if len(data) > 1: + return { + "type": packet_type, + "data": data[1:] + } + + else: + return { + "type": packet_type + } + + # Binary data + packet_type = data[0] + return { + "type": Parser.packet_type_lists[int(packet_type)], + "data": data[1:] + } + + + @staticmethod + def decode_base64_packet(data): + if data[0] == 'b': + data = data[1:] + index = int(data[0]) + packet_type = Parser.packet_type_lists[index] + data = bytearray(base64.standard_b64decode(data[1:])) + return { + "type": packet_type, + "data": data + } + + @staticmethod + def encode_payload(packets, supports_binary=True): + if supports_binary is True: + return Parser.encode_payload_as_binary(packets) + + if not packets: + return '0:' + + if type(packets) not in (tuple, list): + packets = packets, + + out_buffer = bytearray() + for packet in packets: + encoded = Parser.encode_packet(packet, supports_binary) + out_buffer += '{0}:{1}'.format(str(len(encoded)), encoded) + + return out_buffer + + @staticmethod + def decode_payload(data): + if type(data) != str: + for result in Parser.decode_payload_as_binary(data): + yield result + + elif not data: + yield (Parser.error_packet, 0, 1) + + else: + length_str = '' + total_length = len(data) + + for i in xrange(0, len(data)): + ch = data[i] + if ch != ':': + length_str += ch + else: + if length_str == '': + yield (Parser.error_packet, 0, 1) + + length = int(length_str) + message = data[i+1: i+1+length] + + if len(message) != length: + yield (Parser.error_packet, 0, 1) + + if message: + packet = Parser.decode_packet(message) + yield (packet, i + length, total_length) + + length_str = '' + + if length_str != '': + yield (Parser.error_packet, 0, 1) + + + @staticmethod + def encode_payload_as_binary(packets): + if not packets: + return empty_byte_array + + if type(packets) not in (tuple, list): + packets = (packets,) + + out_buffer = bytearray() + for packet in packets: + encoded_packet = Parser.encode_packet(packet, supports_binary=True) + + str_len = str(len(encoded_packet)) + str_len = bytearray([int(c) for c in str_len]) + length_buf = bytearray([0]) + str_len + bytearray([255]) + + if type(encoded_packet) == str: + out_buffer += length_buf + bytearray(encoded_packet) + else: + out_buffer += length_buf + encoded_packet + + return out_buffer + + + @staticmethod + def decode_payload_as_binary(data): + buffer_left = data + + packets = [] + while buffer_left: + str_len = '' + is_string = buffer_left[0] == 0 + + for i in xrange(1, 400): + if buffer_left[i] == 255: + break + + if len(str_len) > 310: + yield (Parser.error_packet, 0, 1) + + str_len += str(buffer_left[i]) + + buffer_left = buffer_left[len(str_len) + 1:] + + message_len = int(str_len) + message = buffer_left[1: message_len + 1] + if is_string: + message = str(message) + + packets.append(Parser.decode_packet(message)) + + buffer_left = buffer_left[message_len + 1:] + + for index, packet in enumerate(packets): + yield (packet, index, len(packets)) diff --git a/socketio/engine/requests.py b/socketio/engine/requests.py new file mode 100644 index 0000000..d6292e8 --- /dev/null +++ b/socketio/engine/requests.py @@ -0,0 +1,12 @@ +# coding=utf-8 +import weakref +from webob.response import Response as WSGIResponse + + +class Request(WSGIResponse): + """ + The request which provides end function to easily end the response and send it out + """ + + def __init__(self, handler, *args, **kwargs): + super(Request, self).__init__(*args, **kwargs) diff --git a/socketio/engine/socket.py b/socketio/engine/socket.py new file mode 100644 index 0000000..3eb1480 --- /dev/null +++ b/socketio/engine/socket.py @@ -0,0 +1,434 @@ +""" +Engine socket, a abstract layer for all transports internal api. It is created by Engine.handler with proper parameters +and used by socketio.socket. +""" + +import random +import weakref +import logging + +import gevent +from gevent.queue import Queue +from gevent.event import Event +from pyee import EventEmitter + +from socketio.defaultjson import default_json_loads, default_json_dumps + + +logger = logging.getLogger(__name__) + + +def default_error_handler(socket, error_name, error_message, endpoint, + msg_id, quiet): + """This is the default error handler, you can override this when + calling :func:`socketio.socketio_manage`. + + It basically sends an event through the socket with the 'error' name. + + See documentation for :meth:`Socket.error`. + + :param quiet: if quiet, this handler will not send a packet to the + user, but only log for the server developer. + """ + pkt = dict(type='event', name='error', + args=[error_name, error_message], + endpoint=endpoint) + if msg_id: + pkt['id'] = msg_id + + # Send an error event through the Socket + if not quiet: + socket.send_packet('event', default_json_dumps(pkt)) + + # Log that error somewhere for debugging... + logger.error(u"default_error_handler: {}, {} (endpoint={}, msg_id={})".format( + error_name, error_message, endpoint, msg_id + )) + + +class Socket(EventEmitter): + """ + The engine socket which handles engine related protocol + """ + + STATE_NEW = "NEW" + STATE_OPENING = "OPENING" + STATE_OPEN = "OPEN" + STATE_CLOSING = "CLOSING" + STATE_CLOSED = "CLOSED" + + json_loads = staticmethod(default_json_loads) + json_dumps = staticmethod(default_json_dumps) + + def __init__(self, transport, ping_interval=5000, error_handler=None): + super(Socket, self).__init__() + + self.sessid = str(random.random())[2:] + self.ready_state = self.STATE_NEW + self.environ = None + self.upgraded = False + + self.write_buffer = [] # queue for messages to client + self.server_queue = Queue() # queue for messages to server + + self.timeout = Event() + self.wsgi_app_greenlet = None + self.send_packet_callbacks = [] + self.jobs = [] + self.error_handler = default_error_handler + self.ping_timeout_eventlet = None + self.check_eventlet = None + self.upgrade_eventlet = None + + self._set_transport(transport) + + if error_handler is not None: + self.error_handler = error_handler + + def _set_transport(self, transport): + self.transport = transport + self.transport.once('error', self.on_error) + self.transport.on('packet', self.on_packet) + self.transport.on('drain', self.flush) + self.transport.once('close', self.on_close) + + def _clear_transport(self): + self.transport.on('error', lambda: logger.debug('error triggered by discarded transport')) + if self.ping_timeout_eventlet: + self.ping_timeout_eventlet.kill() + + def on_open(self): + logger.debug('in on_open socket') + self.ready_state = self.STATE_OPEN + self.send_packet( + "open", + self.json_dumps({ + "sid": self.sessid, + # "upgrades": ["websocket"], + "upgrades": [], + "pingInterval": 15000, + "pingTimeout": 60000}) + ) + self.emit("open") + self._set_ping_timeout_eventlet() + + def on_packet(self, packet): + if self.STATE_OPEN == self.ready_state: + logger.debug("packet") + + self.emit("packet", packet) + self._set_ping_timeout_eventlet() + + packet_type = packet["type"] + + if packet_type == 'ping': + logger.debug("got ping") + self.send_packet('pong') + self.emit("heartbeat") # TODO DO WE REALLY NEED THIS? + + elif packet_type == 'message': + self.emit("message", packet['data']) + + elif packet_type == 'error': + self.on_close("Parse error") + + else: + logger.debug("Packet received with closed socket") + + def on_error(self, error): + logger.debug("transport error: %s" % error) + self.on_close('transport error', error) + + def on_close(self, reason, description=None): + if self.STATE_CLOSED != self.ready_state: + if self.ping_timeout_eventlet: + self.ping_timeout_eventlet.kill() + self.ping_timeout_eventlet = None + + if self.check_eventlet: + self.check_eventlet.kill() + self.check_eventlet = None + + if self.upgrade_eventlet: + self.upgrade_eventlet.kill() + self.upgrade_eventlet = None + + self._clear_transport() + self.ready_state = self.STATE_CLOSED + self.emit("close", reason, description) + self.write_buffer = [] + + def _fail_upgrade(self, transport): + logger.debug('client did not complete upgrade - closing transport') + + # Cancel jobs + self.kill(detach=True) + + if self.check_eventlet: + self.check_eventlet.kill() + self.check_eventlet = None + + if 'open' == transport.readyState: + transport.close() + + def maybe_upgrade(self, transport): + logger.debug("might upgrade from %s to %s" % self.transport.name, transport.name) + + # TODO MAKE TIME OUT CONFIGURABLE + self.upgrade_eventlet = gevent.spawn_later(1, self._fail_upgrade, transport) + + def check(): + if 'polling' == self.transport.name and self.transport.writable: + logger.debug("writing a noop packet to polling for fast upgrade") + self.transport.send([{ + "type": "noop" + }]) + + @transport.on("packet") + def on_packet(packet): + if "ping" == packet["type"] and "probe" == packet["data"]: + transport.send([{ + "type": "pong", + "data": "probe" + }]) + + self.check_eventlet.kill() + + def loop(): + while True: + gevent.sleep(0.1) + check() + + self.check_eventlet = gevent.Greenlet.spawn(loop) + + elif 'upgrade' == packet["type"] and self.ready_state == self.STATE_OPEN: + logger.debug("got upgrade packet - upgrading") + self.upgraded = True + self._clear_transport() + self.emit("upgrade", transport) + self._set_ping_timeout_eventlet() + self.upgrade_eventlet.kill() + self.flush() + transport.remove_listener('packet', on_packet) + + else: + transport.close() + + def _set_ping_timeout_eventlet(self): + if self.ping_timeout_eventlet: + self.ping_timeout_eventlet.kill() + + def time_out(): + self.on_close('ping timeout') + + # TODO THIS IS TIMEOUT + INTERVAL, FIX THIS + self.ping_timeout_eventlet = gevent.spawn_later(4000, time_out) + + def _set_environ(self, environ): + """Save the WSGI environ, for future use. + + This is called by socketio_manage(). + """ + self.environ = environ + + def _set_error_handler(self, error_handler): + """Changes the default error_handler function to the one specified + + This is called by socketio_manage(). + """ + self.error_handler = error_handler + + def _set_json_loads(self, json_loads): + """Change the default JSON decoder. + + This should be a callable that accepts a single string, and returns + a well-formed object. + """ + self.json_loads = json_loads + + def _set_json_dumps(self, json_dumps): + """Change the default JSON decoder. + + This should be a callable that accepts a single string, and returns + a well-formed object. + """ + self.json_dumps = json_dumps + + def _get_next_msgid(self): + """This retrieves the next value for the 'id' field when sending + an 'event' or 'message' or 'json' that asks the remote client + to 'ack' back, so that we trigger the local callback. + """ + self.ack_counter += 1 + return self.ack_counter + + def _save_ack_callback(self, msgid, callback): + """Keep a reference of the callback on this socket.""" + if msgid in self.ack_callbacks: + return False + self.ack_callbacks[msgid] = callback + + def _pop_ack_callback(self, msgid): + """Fetch the callback for a given msgid, if it exists, otherwise, + return None""" + if msgid not in self.ack_callbacks: + return None + return self.ack_callbacks.pop(msgid) + + def __str__(self): + result = ['sessid=%r' % self.sessid] + if self.ready_state == self.STATE_OPEN: + result.append('open') + if self.write_buffer: + result.append('client_queue[%s]' % len(self.write_buffer)) + if self.server_queue.qsize(): + result.append('server_queue[%s]' % self.server_queue.qsize()) + + return ' '.join(result) + + def __getitem__(self, key): + """This will get the nested Namespace using its '/chat' reference. + + Using this, you can go from one Namespace to the other (to emit, add + ACLs, etc..) with: + + adminnamespace.socket['/chat'].add_acl_method('kick-ban') + + """ + return self.active_ns[key] + + def __hasitem__(self, key): + """Verifies if the namespace is active (was initialized)""" + return key in self.active_ns + + def send(self, data): + self.send_packet('message', data) + return self + + write = send + + def send_packet(self, packet_type, data=None, callback=None): + """ + the primary send_packet method + """ + logger.debug('send_packet in socket data [%s]' % data) + packet = { + "type": packet_type + } + + if data: + packet["data"] = data + + self.emit("packet_created", packet) + + if self.ready_state != self.STATE_CLOSING: + self.put_client_msg(packet) + if callback: + self.send_packet_callbacks.append(callback) + self.flush() + + def flush(self): + logger.debug("entering flushing buffer to transport " + str(self.transport.writable) + " " + str(len(self.write_buffer))) + if self.ready_state != self.STATE_CLOSED and self.transport.writable and self.write_buffer: + logger.debug("flushing buffer to transport") + self.emit("flush", self.write_buffer) + local_buf = self.write_buffer + self.write_buffer = [] + + # TODO CALL BACK? + self.transport.send(local_buf) + self.emit('drain') + # TODO SERVER EMIT DRAIN? + + def get_available_upgrades(self): + availabel_upgrades = ["websocket"] + # TODO FIX THIS, HOOK UP WITH SERVER + return availabel_upgrades + + def close(self): + if self.STATE_OPEN == self.ready_state: + self.ready_state = self.STATE_CLOSING + self.transport.close() # TODO transport needs a close method + + def kill(self, detach=False): + """This function must/will be called when a socket is to be completely + shut down, closed by connection timeout, connection error or explicit + disconnection from the client. + + It will call all of the Namespace's + :meth:`~socketio.namespace.BaseNamespace.disconnect` methods + so that you can shut-down things properly. + + """ + # Clear out the callbacks + self.ack_callbacks = {} + if self.STATE_OPEN == self.ready_state: + self.ready_state = self.STATE_CLOSING + self.server_queue.put_nowait(None) + + if detach: + self.detach() + + gevent.killall(self.jobs) + + def detach(self): + """Detach this socket from the server. This should be done in + conjunction with kill(), once all the jobs are dead, detach the + socket for garbage collection.""" + + logger.debug("Removing %s from server sockets" % self) + if self.sessid in self.server.sockets: + self.server.sockets.pop(self.sessid) + + def put_client_msg(self, msg): + """Writes to the client's pipe, to end up in the browser""" + self.write_buffer.append(msg) + + def error(self, error_name, error_message, endpoint=None, msg_id=None, + quiet=False): + """Send an error to the user, using the custom or default + ErrorHandler configured on the [TODO: Revise this] Socket/Handler + object. + + :param error_name: is a simple string, for easy association on + the client side + + :param error_message: is a human readable message, the user + will eventually see + + :param endpoint: set this if you have a message specific to an + end point + + :param msg_id: set this if your error is relative to a + specific message + + :param quiet: way to make the error handler quiet. Specific to + the handler. The default handler will only log, + with quiet. + """ + handler = self.error_handler + return handler( + self, error_name, error_message, endpoint, msg_id, quiet) + + def spawn(self, fn, *args, **kwargs): + """Spawn a new Greenlet, attached to this Socket instance. + + It will be monitored by the "watcher" method + """ + + logger.debug("Spawning sub-Socket Greenlet: %s" % fn.__name__) + job = gevent.spawn(fn, *args, **kwargs) + self.jobs.append(job) + return job + + def _heartbeat(self): + """Start the heartbeat Greenlet to check connection health.""" + interval = self.ping_interval + while Socket.STATE_OPEN == self.ready_state: + gevent.sleep(interval) + # TODO: this process could use a timeout object like the disconnect + # timeout thing, and ONLY send packets when none are sent! + # We would do that by calling timeout.set() for a "sending" + # timeout. If we're sending 100 messages a second, there is + # no need to push some heartbeats in there also. + self.send_packet("ping") diff --git a/socketio/engine/tests/__init__.py b/socketio/engine/tests/__init__.py new file mode 100644 index 0000000..50f2cf9 --- /dev/null +++ b/socketio/engine/tests/__init__.py @@ -0,0 +1 @@ +__author__ = 'lishuo' diff --git a/socketio/engine/tests/test_parser.py b/socketio/engine/tests/test_parser.py new file mode 100644 index 0000000..7267f62 --- /dev/null +++ b/socketio/engine/tests/test_parser.py @@ -0,0 +1,111 @@ +# coding=utf-8 +from unittest import TestCase + +from socketio.engine.parser import Parser + + +class TestParser(TestCase): + def test_encode_payload_as_binary(self): + buffer = Parser.encode_payload_as_binary([ + { + 'type': 'open', + 'data': 'what' + }, + { + 'type': 'message', + 'data': 'hello' + } + ]) + + self.assertEqual(len(buffer), 3 + 1 + 4 + 3 + 1 + 5) + + def test_encode_payload(self): + b = bytearray([0, 1, 2, 3, 4]) + + encoded = Parser.encode_payload([ + { + "type": "message", + "data": b + }, + { + "type": "message", + "data": "hello" + } + ]) + + for packet, index, total in Parser.decode_payload(encoded): + is_last = index + 1 == total + self.assertEqual(packet["type"], "message") + if is_last: + self.assertEqual(packet["data"], "hello") + else: + self.assertEqual(len(packet["data"]), 5) + + def test_encode_binary_message(self): + buffer = bytearray([0, 1, 2, 3, 4]) + encoded_buffer = Parser.encode_packet({ + 'type': 'message', + 'data': buffer + }) + packet = Parser.decode_packet(encoded_buffer) + self.assertEqual(packet['type'], 'message') + self.assertEqual(packet['data'], buffer) + + def test_encode_decode_base64(self): + encoded = Parser.encode_base64_packet({ + "type": "message", + "data": "hello" + }) + + decoded = Parser.decode_base64_packet(encoded) + + self.assertEqual(decoded['type'], 'message') + self.assertEqual(decoded['data'], 'hello') + + def test_encode_binary_as_binary(self): + first_buf = bytearray([0, 1, 2, 3, 4]) + second_buf = bytearray([5, 6, 7, 8]) + + encoded = Parser.encode_payload_as_binary([ + { + "type": "message", + "data": first_buf + }, + { + "type": "message", + "data": second_buf + } + ]) + + for packet, index, total in Parser.decode_payload_as_binary(encoded): + is_last = index + 1 == total + + if is_last: + self.assertEqual(packet["data"], second_buf) + else: + self.assertEqual(packet["data"], first_buf) + + def test_encode_mixed_binary_string_as_binary(self): + buf = bytearray([0, 1, 2, 3, 4]) + + encoded = Parser.encode_payload_as_binary([ + { + "type": "message", + "data": buf + }, + { + "type": "message", + "data": "hello" + }, + { + "type": "close" + } + ]) + + for packet, index, total in Parser.decode_payload_as_binary(encoded): + if index == 0: + self.assertEqual(packet["data"], buf) + elif index == 1: + self.assertEqual(packet["data"], "hello") + elif index == 2: + self.assertFalse("data" in packet) diff --git a/socketio/engine/transports.py b/socketio/engine/transports.py new file mode 100644 index 0000000..f5d7ec3 --- /dev/null +++ b/socketio/engine/transports.py @@ -0,0 +1,345 @@ +import json +import urllib +import urlparse + +from geventwebsocket import WebSocketError +from gevent.queue import Empty +from pyee import EventEmitter + +import logging +import re +from .parser import Parser + +logger = logging.getLogger(__name__) + + +class BaseTransport(EventEmitter): + """ + Base class for all transports. Mostly wraps handler class functions. + + Life cycle for a transport: + A transport object lives cross the whole socket session. + One handler lives for one request, so one transport will survive for + multiple handler objects. + + """ + name = "Base" + + def __init__(self, handler, config, **kwargs): + """Base transport class. + + :param config: dict Should contain the config keys, like + ``heartbeat_interval``, ``heartbeat_timeout`` and + ``close_timeout``. + + """ + + super(BaseTransport, self).__init__() + + self.content_type = ("Content-Type", "text/plain; charset=UTF-8") + self.headers = [ + ("Access-Control-Allow-Origin", "*"), + ("Access-Control-Allow-Credentials", "true"), + ("Access-Control-Allow-Methods", "POST, GET, OPTIONS"), + ("Access-Control-Max-Age", 3600), + ] + + self.supports_binary = config.pop("supports_binary", True) + self.ready_state = "opening" + + self.handler = handler + self.config = config + + self.request = None + self.writable = False + self.should_close = False + + def write(self, data=""): + # Gevent v 0.13 + if hasattr(self.handler, 'response_headers_list'): + if 'Content-Length' not in self.handler.response_headers_list: + self.handler.response_headers.append(('Content-Length', len(data))) + self.handler.response_headers_list.append('Content-Length') + elif not hasattr(self.handler, 'provided_content_length') or self.handler.provided_content_length is None: + # Gevent 1.0bX + l = len(data) + self.handler.provided_content_length = l + self.handler.response_headers.append(('Content-Length', l)) + + self.handler.write_smart(data) + + def _close(self): + raise NotImplementedError() + + def close(self): + self.ready_state = 'closing' + self._close() + + def _cleanup(self): + logger.debug('clean up in transport') + self.handler.remove_listener('cleanup', self._cleanup) + self.request = None + self.handler = None + + def on_handler(self, handler): + self.handler = handler + self.request = handler.request + + self.handler.on("cleanup", self._cleanup) + + def on_error(self, message, description=None): + if self.listeners('error'): + self.emit('error', { + 'type': 'TransportError', + 'description': description + }) + else: + logger.debug("Ignored transoport error %s (%s)" % (message, description)) + + def on_packet(self, packet): + self.emit('packet', packet) + + def on_data(self, data): + self.on_packet(Parser.decode_packet(data)) + + def on_close(self): + self.ready_state = 'closed' + self.emit('close') + + +class PollingTransport(BaseTransport): + name = "polling" + + def __init__(self, *args, **kwargs): + self.data_request = None + super(PollingTransport, self).__init__(*args, **kwargs) + + def on_handler(self, handler): + super(PollingTransport, self).on_handler(handler) + + request = handler.request + if request.method == 'GET': + self.on_poll_request(request) + elif request.method == 'POST': + self.on_data_request(request) + else: + pass + + def _cleanup(self): + self.request = None + self.data_request = None + super(PollingTransport, self)._cleanup() + + def on_poll_request(self, request): + if self.request is not request: + logger.debug('request overlap') + self.on_error('overlap from client') + self.handler.response.status = 500 + return + + logger.debug('setting request') + + self.request = request + # TODO set response? + + # TODO setup response clean up logic + + self.writable = True + self.emit('drain') + + if self.writable and self.should_close: + logger.debug('triggering empty send to append close packet') + self.send([{'type': 'noop'}]) + + def on_data_request(self, request): + """ + The client sends a request with data. + :param request: + :return: + """ + if self.data_request is not request: + self.on_error('data request overlap from client') + # TODO write 500 + return + + is_binary = 'application/octet-stream' == request.headers['content-type'] + self.data_request = request + # TODO SET DATA RESPONSE + # TODO SET UP CLEAN LOGIC + + chunks = bytearray() if is_binary else '' + + chunks += self.data_request.body + self.emit('data', chunks) + self.handler.response.status = 200 + self.handler.response.headers = self.handler.request.headers + self.handler.response.headers.update({ + 'Content-Length': 2, + 'Content-Type': 'text/html' + }) + self.handler.response.body = 'ok' + return + + + def on_data(self, data): + """ + Processes the incoming data payload + :param data: + :return: + """ + + logger.debug('received %s', data) + + for packet in Parser.decode_payload(data): + if packet['type'] == 'close': + logger.debug('got xhr close packet') + # TODO close this + self.on_close() + break + self.on_packet(packet) + + def send(self, packets): + """ + Encode and Send packets + :param packets: The packets list + :return: None + """ + if self.should_close: + packets.push({type: 'close'}) + self.on('should_close') # Use event as callback to do the close logic + self.should_close = False + + encoded = Parser.encode_payload(packets, self.supports_binary) + self.write(encoded) + + def write(self, data=""): + logger.debug('writing %s' % data) + + self.do_write(data) + self.writable = False + + def do_write(self, data): + raise NotImplementedError() + + def do_close(self): + logger.debug('closing') + + if self.data_request: + logger.debug('aborting ongoing data request') + # self.data_request.abort() + + if self.writable: + self.send([{'type': 'close'}]) + + else: + logger.debug('transport not writable - buffering orderly close') + self.should_close = True # TODO SHOULD CLOSE IS A CALLBACK PASSED BY DO_CLOSE + + +class XHRPollingTransport(PollingTransport): + + def on_handler(self, handler): + super(XHRPollingTransport, self).on_handler(handler) + + request = handler.request + if 'OPTIONS' == request.method: + self.handler.response.headers = self.handler.request.headers + self.handler.response.headers['Access-Control-Allow-Headers'] = 'Content-Type' + self.handler.response.status = 200 + + + def do_write(self, data): + is_string = type(data) == str + content_type = 'text/plain; charset=UTF-8' if is_string else 'application/octet-stream' + content_length = str(len(data)) + + headers = { + 'Content-Type': content_type, + 'Content-Length': content_length + } + + ua = self.request.headers['user-agent'] + if ua and (ua.find(';MSIE') == -1 or ua.find('Trident/') == -1): + headers['X-XSS-Protection'] = '0' + + self.handler.response.status = 200 + headers = self.merge_headers(self.request, headers) + self.handler.response.headers = headers + self.handler.response.body = bytes(data) + + def merge_headers(self, request, headers=None): + if not headers: + headers = {} + + if 'origin' in request.headers: + headers['Access-Control-Allow-Credentials'] = 'true' + headers['Access-Control-Allow-Origin'] = request.headers['origin'] + else: + headers['Access-Control-Allow-Origin'] = '*' + + self.emit('headers', headers) + return headers + + +class JSONPollingTransport(PollingTransport): + def __init__(self, request, handler, config): + super(JSONPollingTransport, self).__init__(handler, config) + cn = re.sub('[^0-9]', '', self.request.query['j'] or '') + self.head = '___eio[' + cn + '](' + self.foot = ');' + + def on_data(self, data): + data = urlparse.parse_qsl(data)['d'] + + if type(data) == str: + # TODO ESCAPE HANDLING + super(JSONPollingTransport, self).on_data(data) + + def do_write(self, data): + js = json.dumps(data) + + args = urlparse.parse_qs(self.handler.environ.get("QUERY_STRING")) + if "i" in args: + i = args["i"] + else: + i = "0" + + super(JSONPollingTransport, self).write("io.j[%s]('%s');" % (i, data)) + + +class WebsocketTransport(BaseTransport): + name = 'websocket' + + def do_exchange(self, socket, request_method): + websocket = self.handler.environ['wsgi.websocket'] + websocket.send("1::") # 'connect' packet + + def send_into_ws(): + while True: + message = socket.get_client_msg() + + if message is None: + break + try: + websocket.send(message) + except (WebSocketError, TypeError): + # We can't send a message on the socket + # it is dead, let the other sockets know + socket.disconnect() + + def read_from_ws(): + while True: + message = websocket.receive() + + if message is None: + break + else: + if message is not None: + socket.put_server_msg(message) + + socket.spawn(send_into_ws) + socket.spawn(read_from_ws) + + +class FlashSocketTransport(WebsocketTransport): + pass diff --git a/socketio/handler.py b/socketio/handler.py index e4b972f..71115a6 100644 --- a/socketio/handler.py +++ b/socketio/handler.py @@ -1,225 +1,17 @@ +import copy import sys -import re -import gevent import urlparse +import Cookie +import gevent from gevent.pywsgi import WSGIHandler -from socketio import transports - -class SocketIOHandler(WSGIHandler): - RE_REQUEST_URL = re.compile(r""" - ^/(?P.+?) - /1 - /(?P[^/]+) - /(?P[^/]+)/?$ - """, re.X) - RE_HANDSHAKE_URL = re.compile(r"^/(?P.+?)/1/$", re.X) - # new socket.io versions (> 0.9.8) call an obscure url with two slashes - # instead of a transport when disconnecting - # https://github.com/LearnBoost/socket.io-client/blob/0.9.16/lib/socket.js#L361 - RE_DISCONNECT_URL = re.compile(r""" - ^/(?P.+?) - /(?P[^/]+) - //(?P[^/]+)/?$ - """, re.X) - - handler_types = { - 'websocket': transports.WebsocketTransport, - 'flashsocket': transports.FlashSocketTransport, - 'htmlfile': transports.HTMLFileTransport, - 'xhr-multipart': transports.XHRMultipartTransport, - 'xhr-polling': transports.XHRPollingTransport, - 'jsonp-polling': transports.JSONPolling, - } - - def __init__(self, config, *args, **kwargs): - """Create a new SocketIOHandler. - - :param config: dict Configuration for timeouts and intervals - that will go down to the other components, transports, etc.. - - """ - self.socketio_connection = False - self.allowed_paths = None - self.config = config - - super(SocketIOHandler, self).__init__(*args, **kwargs) - - self.transports = self.handler_types.keys() - if self.server.transports: - self.transports = self.server.transports - if not set(self.transports).issubset(set(self.handler_types)): - raise ValueError("transports should be elements of: %s" % - (self.handler_types.keys())) - - def _do_handshake(self, tokens): - if tokens["resource"] != self.server.resource: - self.log_error("socket.io URL mismatch") - else: - socket = self.server.get_socket() - data = "%s:%s:%s:%s" % (socket.sessid, - self.config['heartbeat_timeout'] or '', - self.config['close_timeout'] or '', - ",".join(self.transports)) - self.write_smart(data) - - def write_jsonp_result(self, data, wrapper="0"): - self.start_response("200 OK", [ - ("Content-Type", "application/javascript"), - ]) - self.result = ['io.j[%s]("%s");' % (wrapper, data)] - - def write_plain_result(self, data): - self.start_response("200 OK", [ - ("Access-Control-Allow-Origin", self.environ.get('HTTP_ORIGIN', '*')), - ("Access-Control-Allow-Credentials", "true"), - ("Access-Control-Allow-Methods", "POST, GET, OPTIONS"), - ("Access-Control-Max-Age", 3600), - ("Content-Type", "text/plain"), - ]) - self.result = [data] - - def write_smart(self, data): - args = urlparse.parse_qs(self.environ.get("QUERY_STRING")) - - if "jsonp" in args: - self.write_jsonp_result(data, args["jsonp"][0]) - else: - self.write_plain_result(data) - - self.process_result() - - def handle_one_response(self): - """This function deals with *ONE INCOMING REQUEST* from the web. - - It will wire and exchange message to the queues for long-polling - methods, otherwise, will stay alive for websockets. - - """ - path = self.environ.get('PATH_INFO') - - # Kick non-socket.io requests to our superclass - if not path.lstrip('/').startswith(self.server.resource + '/'): - return super(SocketIOHandler, self).handle_one_response() - - self.status = None - self.headers_sent = False - self.result = None - self.response_length = 0 - self.response_use_chunked = False - - # This is analyzed for each and every HTTP requests involved - # in the Socket.IO protocol, whether long-running or long-polling - # (read: websocket or xhr-polling methods) - request_method = self.environ.get("REQUEST_METHOD") - request_tokens = self.RE_REQUEST_URL.match(path) - handshake_tokens = self.RE_HANDSHAKE_URL.match(path) - disconnect_tokens = self.RE_DISCONNECT_URL.match(path) - - if handshake_tokens: - # Deal with first handshake here, create the Socket and push - # the config up. - return self._do_handshake(handshake_tokens.groupdict()) - elif disconnect_tokens: - # it's a disconnect request via XHR - tokens = disconnect_tokens.groupdict() - elif request_tokens: - tokens = request_tokens.groupdict() - # and continue... - else: - # This is no socket.io request. Let the WSGI app handle it. - return super(SocketIOHandler, self).handle_one_response() - - # Setup socket - sessid = tokens["sessid"] - socket = self.server.get_socket(sessid) - if not socket: - self.handle_bad_request() - return [] # Do not say the session is not found, just bad request - # so they don't start brute forcing to find open sessions - - if self.environ['QUERY_STRING'].startswith('disconnect'): - # according to socket.io specs disconnect requests - # have a `disconnect` query string - # https://github.com/LearnBoost/socket.io-spec#forced-socket-disconnection - socket.disconnect() - self.handle_disconnect_request() - return [] - - # Setup transport - transport = self.handler_types.get(tokens["transport_id"]) - - # In case this is WebSocket request, switch to the WebSocketHandler - # FIXME: fix this ugly class change - old_class = None - if issubclass(transport, (transports.WebsocketTransport, - transports.FlashSocketTransport)): - old_class = self.__class__ - self.__class__ = self.server.ws_handler_class - self.prevent_wsgi_call = True # thank you - # TODO: any errors, treat them ?? - self.handle_one_response() # does the Websocket dance before we continue - - # Make the socket object available for WSGI apps - self.environ['socketio'] = socket - - # Create a transport and handle the request likewise - self.transport = transport(self, self.config) - - # transports register their own spawn'd jobs now - self.transport.do_exchange(socket, request_method) - - if not socket.connection_established: - # This is executed only on the *first* packet of the establishment - # of the virtual Socket connection. - socket.connection_established = True - socket.state = socket.STATE_CONNECTED - socket._spawn_heartbeat() - socket._spawn_watcher() - - try: - # We'll run the WSGI app if it wasn't already done. - if socket.wsgi_app_greenlet is None: - # TODO: why don't we spawn a call to handle_one_response here ? - # why call directly the WSGI machinery ? - start_response = lambda status, headers, exc=None: None - socket.wsgi_app_greenlet = gevent.spawn(self.application, - self.environ, - start_response) - except: - self.handle_error(*sys.exc_info()) - # we need to keep the connection open if we are an open socket - if tokens['transport_id'] in ['flashsocket', 'websocket']: - # wait here for all jobs to finished, when they are done - gevent.joinall(socket.jobs) +from socketio.engine import transports +from socketio.engine.handler import EngineHandler - # Switch back to the old class so references to this don't use the - # incorrect class. Useful for debugging. - if old_class: - self.__class__ = old_class - # Clean up circular references so they can be garbage collected. - if hasattr(self, 'websocket') and self.websocket: - if hasattr(self.websocket, 'environ'): - del self.websocket.environ - del self.websocket - if self.environ: - del self.environ +class SocketIOHandler(EngineHandler): + pass - def handle_bad_request(self): - self.close_connection = True - self.start_response("400 Bad Request", [ - ('Content-Type', 'text/plain'), - ('Connection', 'close'), - ('Content-Length', 0) - ]) - def handle_disconnect_request(self): - self.close_connection = True - self.start_response("200 OK", [ - ('Content-Type', 'text/plain'), - ('Connection', 'close'), - ('Content-Length', 0) - ]) diff --git a/socketio/server.py b/socketio/server.py index eada8ee..24ba324 100644 --- a/socketio/server.py +++ b/socketio/server.py @@ -1,14 +1,13 @@ import sys import traceback - from socket import error from gevent.pywsgi import WSGIServer +from geventwebsocket.handler import WebSocketHandler from socketio.handler import SocketIOHandler from socketio.policyserver import FlashPolicyServer -from socketio.virtsocket import Socket -from geventwebsocket.handler import WebSocketHandler + __all__ = ['SocketIOServer'] @@ -92,7 +91,6 @@ def __init__(self, *args, **kwargs): if not 'handler_class' in kwargs: kwargs['handler_class'] = SocketIOHandler - if not 'ws_handler_class' in kwargs: self.ws_handler_class = WebSocketHandler else: @@ -127,21 +125,6 @@ def handle(self, socket, address): handler = self.handler_class(self.config, socket, address, self) handler.handle() - def get_socket(self, sessid=''): - """Return an existing or new client Socket.""" - - socket = self.sockets.get(sessid) - - if sessid and not socket: - return None # you ask for a session that doesn't exist! - if socket is None: - socket = Socket(self, self.config) - self.sockets[socket.sessid] = socket - else: - socket.incr_hits() - - return socket - def serve(app, **kw): _quiet = kw.pop('_quiet', False) diff --git a/socketio/transports.py b/socketio/transports.py deleted file mode 100644 index d4b7ab2..0000000 --- a/socketio/transports.py +++ /dev/null @@ -1,317 +0,0 @@ -import gevent -import urllib -import urlparse -from geventwebsocket import WebSocketError -from gevent.queue import Empty - - -class BaseTransport(object): - """Base class for all transports. Mostly wraps handler class functions.""" - - def __init__(self, handler, config, **kwargs): - """Base transport class. - - :param config: dict Should contain the config keys, like - ``heartbeat_interval``, ``heartbeat_timeout`` and - ``close_timeout``. - - """ - self.content_type = ("Content-Type", "text/plain; charset=UTF-8") - self.headers = [ - ("Access-Control-Allow-Origin", "*"), - ("Access-Control-Allow-Credentials", "true"), - ("Access-Control-Allow-Methods", "POST, GET, OPTIONS"), - ("Access-Control-Max-Age", 3600), - ] - self.handler = handler - self.config = config - - def write(self, data=""): - # Gevent v 0.13 - if hasattr(self.handler, 'response_headers_list'): - if 'Content-Length' not in self.handler.response_headers_list: - self.handler.response_headers.append(('Content-Length', len(data))) - self.handler.response_headers_list.append('Content-Length') - elif not hasattr(self.handler, 'provided_content_length') or self.handler.provided_content_length is None: - # Gevent 1.0bX - l = len(data) - self.handler.provided_content_length = l - self.handler.response_headers.append(('Content-Length', l)) - - self.handler.write_smart(data) - - def start_response(self, status, headers, **kwargs): - if "Content-Type" not in [x[0] for x in headers]: - headers.append(self.content_type) - - headers.extend(self.headers) - self.handler.start_response(status, headers, **kwargs) - - -class XHRPollingTransport(BaseTransport): - def __init__(self, *args, **kwargs): - super(XHRPollingTransport, self).__init__(*args, **kwargs) - - def options(self): - self.start_response("200 OK", ()) - self.write() - return [] - - def get(self, socket): - socket.heartbeat() - - heartbeat_interval = self.config['heartbeat_interval'] - payload = self.get_messages_payload(socket, timeout=heartbeat_interval) - if not payload: - payload = "8::" # NOOP - - self.start_response("200 OK", []) - self.write(payload) - - def _request_body(self): - return self.handler.wsgi_input.readline() - - def post(self, socket): - for message in self.decode_payload(self._request_body()): - socket.put_server_msg(message) - - self.start_response("200 OK", [ - ("Connection", "close"), - ("Content-Type", "text/plain") - ]) - self.write("1") - - def get_messages_payload(self, socket, timeout=None): - """This will fetch the messages from the Socket's queue, and if - there are many messes, pack multiple messages in one payload and return - """ - try: - msgs = socket.get_multiple_client_msgs(timeout=timeout) - data = self.encode_payload(msgs) - except Empty: - data = "" - return data - - def encode_payload(self, messages): - """Encode list of messages. Expects messages to be unicode. - - ``messages`` - List of raw messages to encode, if necessary - - """ - if not messages or messages[0] is None: - return '' - - if len(messages) == 1: - return messages[0].encode('utf-8') - - payload = u''.join([(u'\ufffd%d\ufffd%s' % (len(p), p)) - for p in messages if p is not None]) - # FIXME: why is it so that we must filter None from here ? How - # is it even possible that a None gets in there ? - - return payload.encode('utf-8') - - def decode_payload(self, payload): - """This function can extract multiple messages from one HTTP payload. - Some times, the XHR/JSONP/.. transports can pack more than one message - on a single packet. They are encoding following the WebSocket - semantics, which need to be reproduced here to unwrap the messages. - - The semantics are: - - \ufffd + [length as a string] + \ufffd + [payload as a unicode string] - - This function returns a list of messages, even though there is only - one. - - Inspired by socket.io/lib/transports/http.js - """ - payload = payload.decode('utf-8') - if payload[0] == u"\ufffd": - ret = [] - while len(payload) != 0: - len_end = payload.find(u"\ufffd", 1) - length = int(payload[1:len_end]) - msg_start = len_end + 1 - msg_end = length + msg_start - message = payload[msg_start:msg_end] - ret.append(message) - payload = payload[msg_end:] - return ret - return [payload] - - def do_exchange(self, socket, request_method): - if not socket.connection_established: - # Runs only the first time we get a Socket opening - self.start_response("200 OK", [ - ("Connection", "close"), - ]) - self.write("1::") # 'connect' packet - return - elif request_method in ("GET", "POST", "OPTIONS"): - return getattr(self, request_method.lower())(socket) - else: - raise Exception("No support for the method: " + request_method) - - -class JSONPolling(XHRPollingTransport): - def __init__(self, handler, config): - super(JSONPolling, self).__init__(handler, config) - self.content_type = ("Content-Type", "text/javascript; charset=UTF-8") - - def _request_body(self): - data = super(JSONPolling, self)._request_body() - # resolve %20%3F's, take out wrapping d="...", etc.. - data = urllib.unquote_plus(data)[3:-1] \ - .replace(r'\"', '"') \ - .replace(r"\\", "\\") - - # For some reason, in case of multiple messages passed in one - # query, IE7 sends it escaped, not utf-8 encoded. This dirty - # hack handled it - if data[0] == "\\": - data = data.decode("unicode_escape").encode("utf-8") - return data - - def write(self, data): - """Just quote out stuff before sending it out""" - args = urlparse.parse_qs(self.handler.environ.get("QUERY_STRING")) - if "i" in args: - i = args["i"] - else: - i = "0" - # TODO: don't we need to quote this data in here ? - super(JSONPolling, self).write("io.j[%s]('%s');" % (i, data)) - - -class XHRMultipartTransport(XHRPollingTransport): - def __init__(self, handler): - super(JSONPolling, self).__init__(handler) - self.content_type = ( - "Content-Type", - "multipart/x-mixed-replace;boundary=\"socketio\"" - ) - - def do_exchange(self, socket, request_method): - if request_method == "GET": - return self.get(socket) - elif request_method == "POST": - return self.post(socket) - else: - raise Exception("No support for such method: " + request_method) - - def get(self, socket): - header = "Content-Type: text/plain; charset=UTF-8\r\n\r\n" - - self.start_response("200 OK", [("Connection", "keep-alive")]) - self.write_multipart("--socketio\r\n") - self.write_multipart(header) - self.write_multipart(str(socket.sessid) + "\r\n") - self.write_multipart("--socketio\r\n") - - def chunk(): - while True: - payload = self.get_messages_payload(socket) - - if not payload: - # That would mean the call to Queue.get() returned Empty, - # so it was in fact killed, since we pass no timeout=.. - return - # See below - else: - try: - self.write_multipart(header) - self.write_multipart(payload) - self.write_multipart("--socketio\r\n") - except socket.error: - # The client might try to reconnect, even with a socket - # error, so let's just let it go, and not kill the - # socket completely. Other processes will ensure - # we kill everything if the user expires the timeouts. - # - # WARN: this means that this payload is LOST, unless we - # decide to re-inject it into the queue. - return - - socket.spawn(chunk) - - -class WebsocketTransport(BaseTransport): - def do_exchange(self, socket, request_method): - websocket = self.handler.environ['wsgi.websocket'] - websocket.send("1::") # 'connect' packet - - def send_into_ws(): - while True: - message = socket.get_client_msg() - - if message is None: - break - try: - websocket.send(message) - except (WebSocketError, TypeError): - # We can't send a message on the socket - # it is dead, let the other sockets know - socket.disconnect() - - def read_from_ws(): - while True: - message = websocket.receive() - - if message is None: - break - else: - if message is not None: - socket.put_server_msg(message) - - socket.spawn(send_into_ws) - socket.spawn(read_from_ws) - - -class FlashSocketTransport(WebsocketTransport): - pass - - -class HTMLFileTransport(XHRPollingTransport): - """Not tested at all!""" - - def __init__(self, handler, config): - super(HTMLFileTransport, self).__init__(handler, config) - self.content_type = ("Content-Type", "text/html") - - def write_packed(self, data): - self.write("" % data) - - def write(self, data): - l = 1024 * 5 - super(HTMLFileTransport, self).write("%d\r\n%s%s\r\n" % (l, data, " " * (l - len(data)))) - - def do_exchange(self, socket, request_method): - return super(HTMLFileTransport, self).do_exchange(socket, request_method) - - def get(self, socket): - self.start_response("200 OK", [ - ("Connection", "keep-alive"), - ("Content-Type", "text/html"), - ("Transfer-Encoding", "chunked"), - ]) - self.write("") - self.write_packed("1::") # 'connect' packet - - - def chunk(): - while True: - payload = self.get_messages_payload(socket) - - if not payload: - # That would mean the call to Queue.get() returned Empty, - # so it was in fact killed, since we pass no timeout=.. - return - else: - try: - self.write_packed(payload) - except socket.error: - # See comments for XHRMultipart - return - - socket.spawn(chunk) diff --git a/socketio/virtsocket.py b/socketio/virtsocket.py deleted file mode 100644 index bd79c01..0000000 --- a/socketio/virtsocket.py +++ /dev/null @@ -1,479 +0,0 @@ -"""Virtual Socket implementation, unifies all the Transports into one -single interface, and abstracts the work of the long-polling methods. - -This module also has the ``default_error_handler`` implementation. -You can define your own so that the error messages are logged or sent -in a different way - -:copyright: 2012, Alexandre Bourget -:moduleauthor: Alexandre Bourget - -""" -import random -import weakref -import logging - -import gevent -from gevent.queue import Queue -from gevent.event import Event - -from socketio import packet -from socketio.defaultjson import default_json_loads, default_json_dumps - - -log = logging.getLogger(__name__) - - -def default_error_handler(socket, error_name, error_message, endpoint, - msg_id, quiet): - """This is the default error handler, you can override this when - calling :func:`socketio.socketio_manage`. - - It basically sends an event through the socket with the 'error' name. - - See documentation for :meth:`Socket.error`. - - :param quiet: if quiet, this handler will not send a packet to the - user, but only log for the server developer. - """ - pkt = dict(type='event', name='error', - args=[error_name, error_message], - endpoint=endpoint) - if msg_id: - pkt['id'] = msg_id - - # Send an error event through the Socket - if not quiet: - socket.send_packet(pkt) - - # Log that error somewhere for debugging... - log.error(u"default_error_handler: {}, {} (endpoint={}, msg_id={})".format( - error_name, error_message, endpoint, msg_id - )) - - -class Socket(object): - """ - Virtual Socket implementation, checks heartbeats, writes to local queues - for message passing, holds the Namespace objects, dispatches de packets - to the underlying namespaces. - - This is the abstraction on top of the different transports. It's like - if you used a WebSocket only... - """ - - STATE_CONNECTING = "CONNECTING" - STATE_CONNECTED = "CONNECTED" - STATE_DISCONNECTING = "DISCONNECTING" - STATE_DISCONNECTED = "DISCONNECTED" - - GLOBAL_NS = '' - """Use this to be explicit when specifying a Global Namespace (an endpoint - with no name, not '/chat' or anything.""" - - json_loads = staticmethod(default_json_loads) - json_dumps = staticmethod(default_json_dumps) - - def __init__(self, server, config, error_handler=None): - self.server = weakref.proxy(server) - self.sessid = str(random.random())[2:] - self.session = {} # the session dict, for general developer usage - self.client_queue = Queue() # queue for messages to client - self.server_queue = Queue() # queue for messages to server - self.hits = 0 - self.heartbeats = 0 - self.timeout = Event() - self.wsgi_app_greenlet = None - self.state = "NEW" - self.connection_established = False - self.ack_callbacks = {} - self.ack_counter = 0 - self.request = None - self.environ = None - self.namespaces = {} - self.active_ns = {} # Namespace sessions that were instantiated - self.jobs = [] - self.error_handler = default_error_handler - self.config = config - if error_handler is not None: - self.error_handler = error_handler - - def _set_namespaces(self, namespaces): - """This is a mapping (dict) of the different '/namespaces' to their - BaseNamespace object derivative. - - This is called by socketio_manage().""" - self.namespaces = namespaces - - def _set_request(self, request): - """Saves the request object for future use by the different Namespaces. - - This is called by socketio_manage(). - """ - self.request = request - - def _set_environ(self, environ): - """Save the WSGI environ, for future use. - - This is called by socketio_manage(). - """ - self.environ = environ - - def _set_error_handler(self, error_handler): - """Changes the default error_handler function to the one specified - - This is called by socketio_manage(). - """ - self.error_handler = error_handler - - def _set_json_loads(self, json_loads): - """Change the default JSON decoder. - - This should be a callable that accepts a single string, and returns - a well-formed object. - """ - self.json_loads = json_loads - - def _set_json_dumps(self, json_dumps): - """Change the default JSON decoder. - - This should be a callable that accepts a single string, and returns - a well-formed object. - """ - self.json_dumps = json_dumps - - def _get_next_msgid(self): - """This retrieves the next value for the 'id' field when sending - an 'event' or 'message' or 'json' that asks the remote client - to 'ack' back, so that we trigger the local callback. - """ - self.ack_counter += 1 - return self.ack_counter - - def _save_ack_callback(self, msgid, callback): - """Keep a reference of the callback on this socket.""" - if msgid in self.ack_callbacks: - return False - self.ack_callbacks[msgid] = callback - - def _pop_ack_callback(self, msgid): - """Fetch the callback for a given msgid, if it exists, otherwise, - return None""" - if msgid not in self.ack_callbacks: - return None - return self.ack_callbacks.pop(msgid) - - def __str__(self): - result = ['sessid=%r' % self.sessid] - if self.state == self.STATE_CONNECTED: - result.append('connected') - if self.client_queue.qsize(): - result.append('client_queue[%s]' % self.client_queue.qsize()) - if self.server_queue.qsize(): - result.append('server_queue[%s]' % self.server_queue.qsize()) - if self.hits: - result.append('hits=%s' % self.hits) - if self.heartbeats: - result.append('heartbeats=%s' % self.heartbeats) - - return ' '.join(result) - - def __getitem__(self, key): - """This will get the nested Namespace using its '/chat' reference. - - Using this, you can go from one Namespace to the other (to emit, add - ACLs, etc..) with: - - adminnamespace.socket['/chat'].add_acl_method('kick-ban') - - """ - return self.active_ns[key] - - def __hasitem__(self, key): - """Verifies if the namespace is active (was initialized)""" - return key in self.active_ns - - @property - def connected(self): - """Returns whether the state is CONNECTED or not.""" - return self.state == self.STATE_CONNECTED - - def incr_hits(self): - self.hits += 1 - - def heartbeat(self): - """This makes the heart beat for another X seconds. Call this when - you get a heartbeat packet in. - - This clear the heartbeat disconnect timeout (resets for X seconds). - """ - self.timeout.set() - - def kill(self, detach=False): - """This function must/will be called when a socket is to be completely - shut down, closed by connection timeout, connection error or explicit - disconnection from the client. - - It will call all of the Namespace's - :meth:`~socketio.namespace.BaseNamespace.disconnect` methods - so that you can shut-down things properly. - - """ - # Clear out the callbacks - self.ack_callbacks = {} - if self.connected: - self.state = self.STATE_DISCONNECTING - self.server_queue.put_nowait(None) - self.client_queue.put_nowait(None) - if len(self.active_ns) > 0: - log.debug("Calling disconnect() on %s" % self) - self.disconnect() - - if detach: - self.detach() - - gevent.killall(self.jobs) - - def detach(self): - """Detach this socket from the server. This should be done in - conjunction with kill(), once all the jobs are dead, detach the - socket for garbage collection.""" - - log.debug("Removing %s from server sockets" % self) - if self.sessid in self.server.sockets: - self.server.sockets.pop(self.sessid) - - def put_server_msg(self, msg): - """Writes to the server's pipe, to end up in in the Namespaces""" - self.heartbeat() - self.server_queue.put_nowait(msg) - - def put_client_msg(self, msg): - """Writes to the client's pipe, to end up in the browser""" - self.client_queue.put_nowait(msg) - - def get_client_msg(self, **kwargs): - """Grab a message to send it to the browser""" - return self.client_queue.get(**kwargs) - - def get_server_msg(self, **kwargs): - """Grab a message, to process it by the server and dispatch calls - """ - return self.server_queue.get(**kwargs) - - def get_multiple_client_msgs(self, **kwargs): - """Get multiple messages, in case we're going through the various - XHR-polling methods, on which we can pack more than one message if the - rate is high, and encode the payload for the HTTP channel.""" - client_queue = self.client_queue - msgs = [client_queue.get(**kwargs)] - while client_queue.qsize(): - msgs.append(client_queue.get()) - return msgs - - def error(self, error_name, error_message, endpoint=None, msg_id=None, - quiet=False): - """Send an error to the user, using the custom or default - ErrorHandler configured on the [TODO: Revise this] Socket/Handler - object. - - :param error_name: is a simple string, for easy association on - the client side - - :param error_message: is a human readable message, the user - will eventually see - - :param endpoint: set this if you have a message specific to an - end point - - :param msg_id: set this if your error is relative to a - specific message - - :param quiet: way to make the error handler quiet. Specific to - the handler. The default handler will only log, - with quiet. - """ - handler = self.error_handler - return handler( - self, error_name, error_message, endpoint, msg_id, quiet) - - # User facing low-level function - def disconnect(self, silent=False): - """Calling this method will call the - :meth:`~socketio.namespace.BaseNamespace.disconnect` method on - all the active Namespaces that were open, killing all their - jobs and sending 'disconnect' packets for each of them. - - Normally, the Global namespace (endpoint = '') has special meaning, - as it represents the whole connection, - - :param silent: when True, pass on the ``silent`` flag to the Namespace - :meth:`~socketio.namespace.BaseNamespace.disconnect` - calls. - """ - for ns_name, ns in list(self.active_ns.iteritems()): - ns.recv_disconnect() - - def remove_namespace(self, namespace): - """This removes a Namespace object from the socket. - - This is usually called by - :meth:`~socketio.namespace.BaseNamespace.disconnect`. - - """ - if namespace in self.active_ns: - del self.active_ns[namespace] - - if len(self.active_ns) == 0 and self.connected: - self.kill(detach=True) - - def send_packet(self, pkt): - """Low-level interface to queue a packet on the wire (encoded as wire - protocol""" - self.put_client_msg(packet.encode(pkt, self.json_dumps)) - - def spawn(self, fn, *args, **kwargs): - """Spawn a new Greenlet, attached to this Socket instance. - - It will be monitored by the "watcher" method - """ - - log.debug("Spawning sub-Socket Greenlet: %s" % fn.__name__) - job = gevent.spawn(fn, *args, **kwargs) - self.jobs.append(job) - return job - - def _receiver_loop(self): - """This is the loop that takes messages from the queue for the server - to consume, decodes them and dispatches them. - - It is the main loop for a socket. We join on this process before - returning control to the web framework. - - This process is not tracked by the socket itself, it is not going - to be killed by the ``gevent.killall(socket.jobs)``, so it must - exit gracefully itself. - """ - - while True: - rawdata = self.get_server_msg() - - if not rawdata: - continue # or close the connection ? - try: - pkt = packet.decode(rawdata, self.json_loads) - except (ValueError, KeyError, Exception), e: - self.error('invalid_packet', - "There was a decoding error when dealing with packet " - "with event: %s... (%s)" % (rawdata[:20], e)) - continue - - if pkt['type'] == 'heartbeat': - # This is already dealth with in put_server_msg() when - # any incoming raw data arrives. - continue - - if pkt['type'] == 'disconnect' and pkt['endpoint'] == '': - # On global namespace, we kill everything. - self.kill(detach=True) - continue - - endpoint = pkt['endpoint'] - - if endpoint not in self.namespaces: - self.error("no_such_namespace", - "The endpoint you tried to connect to " - "doesn't exist: %s" % endpoint, endpoint=endpoint) - continue - elif endpoint in self.active_ns: - pkt_ns = self.active_ns[endpoint] - else: - new_ns_class = self.namespaces[endpoint] - pkt_ns = new_ns_class(self.environ, endpoint, - request=self.request) - # This calls initialize() on all the classes and mixins, etc.. - # in the order of the MRO - for cls in type(pkt_ns).__mro__: - if hasattr(cls, 'initialize'): - cls.initialize(pkt_ns) # use this instead of __init__, - # for less confusion - - self.active_ns[endpoint] = pkt_ns - - retval = pkt_ns.process_packet(pkt) - - # Has the client requested an 'ack' with the reply parameters ? - if pkt.get('ack') == "data" and pkt.get('id'): - if type(retval) is tuple: - args = list(retval) - else: - args = [retval] - returning_ack = dict(type='ack', ackId=pkt['id'], - args=args, - endpoint=pkt.get('endpoint', '')) - self.send_packet(returning_ack) - - # Now, are we still connected ? - if not self.connected: - self.kill(detach=True) # ?? what,s the best clean-up - # when its not a - # user-initiated disconnect - return - - def _spawn_receiver_loop(self): - """Spawns the reader loop. This is called internall by - socketio_manage(). - """ - job = gevent.spawn(self._receiver_loop) - self.jobs.append(job) - return job - - def _watcher(self): - """Watch out if we've been disconnected, in that case, kill - all the jobs. - - """ - while True: - gevent.sleep(1.0) - if not self.connected: - for ns_name, ns in list(self.active_ns.iteritems()): - ns.recv_disconnect() - # Killing Socket-level jobs - gevent.killall(self.jobs) - break - - def _spawn_watcher(self): - """This one is not waited for with joinall(socket.jobs), as it - is an external watcher, to clean up when everything is done.""" - job = gevent.spawn(self._watcher) - return job - - def _heartbeat(self): - """Start the heartbeat Greenlet to check connection health.""" - interval = self.config['heartbeat_interval'] - while self.connected: - gevent.sleep(interval) - # TODO: this process could use a timeout object like the disconnect - # timeout thing, and ONLY send packets when none are sent! - # We would do that by calling timeout.set() for a "sending" - # timeout. If we're sending 100 messages a second, there is - # no need to push some heartbeats in there also. - self.put_client_msg("2::") - - def _heartbeat_timeout(self): - timeout = float(self.config['heartbeat_timeout']) - while True: - self.timeout.clear() - gevent.sleep(0) - wait_res = self.timeout.wait(timeout=timeout) - if not wait_res: - if self.connected: - log.debug("heartbeat timed out, killing socket") - self.kill(detach=True) - return - - - def _spawn_heartbeat(self): - """This functions returns a list of jobs""" - self.spawn(self._heartbeat) - self.spawn(self._heartbeat_timeout) diff --git a/tests/jstests/jstests.py b/tests/jstests/jstests.py index b21d3b7..5310855 100755 --- a/tests/jstests/jstests.py +++ b/tests/jstests/jstests.py @@ -1,8 +1,21 @@ -from gevent import monkey; monkey.patch_all() +from gevent import monkey; +import sys + +monkey.patch_all() from socketio import socketio_manage from socketio.server import SocketIOServer from socketio.namespace import BaseNamespace +import logging + +root = logging.getLogger() +root.setLevel(logging.DEBUG) + +ch = logging.StreamHandler(sys.stdout) +ch.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +ch.setFormatter(formatter) +root.addHandler(ch) TestHtml = """ @@ -16,7 +29,6 @@
- @@ -30,6 +42,7 @@ def on_requestack(self, val): def on_requestackonevalue(self, val): return val + class Application(object): def __init__(self): self.buffer = [] diff --git a/tests/jstests/static/socket.io.js b/tests/jstests/static/socket.io.js index 39c7272..8f1b2b9 100644 --- a/tests/jstests/static/socket.io.js +++ b/tests/jstests/static/socket.io.js @@ -1,3874 +1,6172 @@ -/*! Socket.IO.js build:0.9.16, development. Copyright(c) 2011 LearnBoost MIT Licensed */ +!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.io=e():"undefined"!=typeof global?global.io=e():"undefined"!=typeof self&&(self.io=e())}(function(){var define,module,exports; +return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o - * MIT Licensed + * Module dependencies. */ -(function (exports, global) { - - /** - * IO namespace. - * - * @namespace - */ - - var io = exports; - - /** - * Socket.IO version - * - * @api public - */ +var url = require('./url'); +var parser = require('socket.io-parser'); +var Manager = require('./manager'); +var debug = require('debug')('socket.io-client'); - io.version = '0.9.16'; - - /** - * Protocol implemented. - * - * @api public - */ - - io.protocol = 1; - - /** - * Available transports, these will be populated with the available transports - * - * @api public - */ +/** + * Module exports. + */ - io.transports = []; +module.exports = exports = lookup; - /** - * Keep track of jsonp callbacks. - * - * @api private - */ +/** + * Managers cache. + */ - io.j = []; +var cache = exports.managers = {}; - /** - * Keep track of our io.Sockets - * - * @api private - */ - io.sockets = {}; +/** + * Looks up an existing `Manager` for multiplexing. + * If the user summons: + * + * `io('http://localhost/a');` + * `io('http://localhost/b');` + * + * We reuse the existing instance based on same scheme/port/host, + * and we initialize sockets for each namespace. + * + * @api public + */ +function lookup(uri, opts) { + if (typeof uri == 'object') { + opts = uri; + uri = undefined; + } - /** - * Manages connections to hosts. - * - * @param {String} uri - * @Param {Boolean} force creation of new socket (defaults to false) - * @api public - */ + opts = opts || {}; - io.connect = function (host, details) { - var uri = io.util.parseUri(host) - , uuri - , socket; + var parsed = url(uri); + var source = parsed.source; + var id = parsed.id; + var io; - if (global && global.location) { - uri.protocol = uri.protocol || global.location.protocol.slice(0, -1); - uri.host = uri.host || (global.document - ? global.document.domain : global.location.hostname); - uri.port = uri.port || global.location.port; + if (opts.forceNew || opts['force new connection'] || false === opts.multiplex) { + debug('ignoring socket cache for %s', source); + io = Manager(source, opts); + } else { + if (!cache[id]) { + debug('new io instance for %s', source); + cache[id] = Manager(source, opts); } + io = cache[id]; + } - uuri = io.util.uniqueUri(uri); - - var options = { - host: uri.host - , secure: 'https' == uri.protocol - , port: uri.port || ('https' == uri.protocol ? 443 : 80) - , query: uri.query || '' - }; - - io.util.merge(options, details); + return io.socket(parsed.path); +} - if (options['force new connection'] || !io.sockets[uuri]) { - socket = new io.Socket(options); - } +/** + * Protocol version. + * + * @api public + */ - if (!options['force new connection'] && socket) { - io.sockets[uuri] = socket; - } +exports.protocol = parser.protocol; - socket = socket || io.sockets[uuri]; +/** + * `connect`. + * + * @param {String} uri + * @api public + */ - // if path is different from '' or / - return socket.of(uri.path.length > 1 ? uri.path : ''); - }; +exports.connect = lookup; -})('object' === typeof module ? module.exports : (this.io = {}), this); /** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed + * Expose constructors for standalone build. + * + * @api public */ -(function (exports, global) { +exports.Manager = require('./manager'); +exports.Socket = require('./socket'); - /** - * Utilities namespace. - * - * @namespace - */ +},{"./manager":3,"./socket":5,"./url":6,"debug":9,"socket.io-parser":40}],3:[function(require,module,exports){ - var util = exports.util = {}; +/** + * Module dependencies. + */ - /** - * Parses an URI - * - * @author Steven Levithan (MIT license) - * @api public - */ +var url = require('./url'); +var eio = require('engine.io-client'); +var Socket = require('./socket'); +var Emitter = require('component-emitter'); +var parser = require('socket.io-parser'); +var on = require('./on'); +var bind = require('component-bind'); +var object = require('object-component'); +var debug = require('debug')('socket.io-client:manager'); - var re = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/; +/** + * Module exports + */ - var parts = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', - 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', - 'anchor']; +module.exports = Manager; - util.parseUri = function (str) { - var m = re.exec(str || '') - , uri = {} - , i = 14; +/** + * `Manager` constructor. + * + * @param {String} engine instance or engine uri/opts + * @param {Object} options + * @api public + */ - while (i--) { - uri[parts[i]] = m[i] || ''; - } +function Manager(uri, opts){ + if (!(this instanceof Manager)) return new Manager(uri, opts); + if (uri && ('object' == typeof uri)) { + opts = uri; + uri = undefined; + } + opts = opts || {}; + + opts.path = opts.path || '/socket.io'; + this.nsps = {}; + this.subs = []; + this.opts = opts; + this.reconnection(opts.reconnection !== false); + this.reconnectionAttempts(opts.reconnectionAttempts || Infinity); + this.reconnectionDelay(opts.reconnectionDelay || 1000); + this.reconnectionDelayMax(opts.reconnectionDelayMax || 5000); + this.timeout(null == opts.timeout ? 20000 : opts.timeout); + this.readyState = 'closed'; + this.uri = uri; + this.connected = 0; + this.attempts = 0; + this.encoding = false; + this.packetBuffer = []; + this.encoder = new parser.Encoder(); + this.decoder = new parser.Decoder(); + this.open(); +} - return uri; - }; +/** + * Propagate given event to sockets and emit on `this` + * + * @api private + */ - /** - * Produces a unique url that identifies a Socket.IO connection. - * - * @param {Object} uri - * @api public - */ - - util.uniqueUri = function (uri) { - var protocol = uri.protocol - , host = uri.host - , port = uri.port; - - if ('document' in global) { - host = host || document.domain; - port = port || (protocol == 'https' - && document.location.protocol !== 'https:' ? 443 : document.location.port); - } else { - host = host || 'localhost'; +Manager.prototype.emitAll = function() { + this.emit.apply(this, arguments); + for (var nsp in this.nsps) { + this.nsps[nsp].emit.apply(this.nsps[nsp], arguments); + } +}; - if (!port && protocol == 'https') { - port = 443; - } - } +/** + * Mix in `Emitter`. + */ - return (protocol || 'http') + '://' + host + ':' + (port || 80); - }; +Emitter(Manager.prototype); - /** - * Mergest 2 query strings in to once unique query string - * - * @param {String} base - * @param {String} addition - * @api public - */ - - util.query = function (base, addition) { - var query = util.chunkQuery(base || '') - , components = []; - - util.merge(query, util.chunkQuery(addition || '')); - for (var part in query) { - if (query.hasOwnProperty(part)) { - components.push(part + '=' + query[part]); - } - } +/** + * Sets the `reconnection` config. + * + * @param {Boolean} true/false if it should automatically reconnect + * @return {Manager} self or value + * @api public + */ - return components.length ? '?' + components.join('&') : ''; - }; +Manager.prototype.reconnection = function(v){ + if (!arguments.length) return this._reconnection; + this._reconnection = !!v; + return this; +}; - /** - * Transforms a querystring in to an object - * - * @param {String} qs - * @api public - */ - - util.chunkQuery = function (qs) { - var query = {} - , params = qs.split('&') - , i = 0 - , l = params.length - , kv; - - for (; i < l; ++i) { - kv = params[i].split('='); - if (kv[0]) { - query[kv[0]] = kv[1]; - } - } +/** + * Sets the reconnection attempts config. + * + * @param {Number} max reconnection attempts before giving up + * @return {Manager} self or value + * @api public + */ - return query; - }; +Manager.prototype.reconnectionAttempts = function(v){ + if (!arguments.length) return this._reconnectionAttempts; + this._reconnectionAttempts = v; + return this; +}; - /** - * Executes the given function when the page is loaded. - * - * io.util.load(function () { console.log('page loaded'); }); - * - * @param {Function} fn - * @api public - */ +/** + * Sets the delay between reconnections. + * + * @param {Number} delay + * @return {Manager} self or value + * @api public + */ - var pageLoaded = false; +Manager.prototype.reconnectionDelay = function(v){ + if (!arguments.length) return this._reconnectionDelay; + this._reconnectionDelay = v; + return this; +}; - util.load = function (fn) { - if ('document' in global && document.readyState === 'complete' || pageLoaded) { - return fn(); - } +/** + * Sets the maximum delay between reconnections. + * + * @param {Number} delay + * @return {Manager} self or value + * @api public + */ - util.on(global, 'load', fn, false); - }; +Manager.prototype.reconnectionDelayMax = function(v){ + if (!arguments.length) return this._reconnectionDelayMax; + this._reconnectionDelayMax = v; + return this; +}; - /** - * Adds an event. - * - * @api private - */ +/** + * Sets the connection timeout. `false` to disable + * + * @return {Manager} self or value + * @api public + */ - util.on = function (element, event, fn, capture) { - if (element.attachEvent) { - element.attachEvent('on' + event, fn); - } else if (element.addEventListener) { - element.addEventListener(event, fn, capture); - } - }; +Manager.prototype.timeout = function(v){ + if (!arguments.length) return this._timeout; + this._timeout = v; + return this; +}; - /** - * Generates the correct `XMLHttpRequest` for regular and cross domain requests. - * - * @param {Boolean} [xdomain] Create a request that can be used cross domain. - * @returns {XMLHttpRequest|false} If we can create a XMLHttpRequest. - * @api private - */ +/** + * Starts trying to reconnect if reconnection is enabled and we have not + * started reconnecting yet + * + * @api private + */ - util.request = function (xdomain) { +Manager.prototype.maybeReconnectOnOpen = function() { + if (!this.openReconnect && !this.reconnecting && this._reconnection) { + // keeps reconnection from firing twice for the same reconnection loop + this.openReconnect = true; + this.reconnect(); + } +}; - if (xdomain && 'undefined' != typeof XDomainRequest && !util.ua.hasCORS) { - return new XDomainRequest(); - } - if ('undefined' != typeof XMLHttpRequest && (!xdomain || util.ua.hasCORS)) { - return new XMLHttpRequest(); - } +/** + * Sets the current transport `socket`. + * + * @param {Function} optional, callback + * @return {Manager} self + * @api public + */ - if (!xdomain) { - try { - return new window[(['Active'].concat('Object').join('X'))]('Microsoft.XMLHTTP'); - } catch(e) { } +Manager.prototype.open = +Manager.prototype.connect = function(fn){ + debug('readyState %s', this.readyState); + if (~this.readyState.indexOf('open')) return this; + + debug('opening %s', this.uri); + this.engine = eio(this.uri, this.opts); + var socket = this.engine; + var self = this; + this.readyState = 'opening'; + + // emit `open` + var openSub = on(socket, 'open', function() { + self.onopen(); + fn && fn(); + }); + + // emit `connect_error` + var errorSub = on(socket, 'error', function(data){ + debug('connect_error'); + self.cleanup(); + self.readyState = 'closed'; + self.emitAll('connect_error', data); + if (fn) { + var err = new Error('Connection error'); + err.data = data; + fn(err); } - return null; - }; - - /** - * XHR based transport constructor. - * - * @constructor - * @api public - */ - - /** - * Change the internal pageLoaded value. - */ - - if ('undefined' != typeof window) { - util.load(function () { - pageLoaded = true; + self.maybeReconnectOnOpen(); + }); + + // emit `connect_timeout` + if (false !== this._timeout) { + var timeout = this._timeout; + debug('connect attempt will timeout after %d', timeout); + + // set timer + var timer = setTimeout(function(){ + debug('connect attempt timed out after %d', timeout); + openSub.destroy(); + socket.close(); + socket.emit('error', 'timeout'); + self.emitAll('connect_timeout', timeout); + }, timeout); + + this.subs.push({ + destroy: function(){ + clearTimeout(timer); + } }); } - /** - * Defers a function to ensure a spinner is not displayed by the browser - * - * @param {Function} fn - * @api public - */ - - util.defer = function (fn) { - if (!util.ua.webkit || 'undefined' != typeof importScripts) { - return fn(); - } - - util.load(function () { - setTimeout(fn, 100); - }); - }; - - /** - * Merges two objects. - * - * @api public - */ - - util.merge = function merge (target, additional, deep, lastseen) { - var seen = lastseen || [] - , depth = typeof deep == 'undefined' ? 2 : deep - , prop; - - for (prop in additional) { - if (additional.hasOwnProperty(prop) && util.indexOf(seen, prop) < 0) { - if (typeof target[prop] !== 'object' || !depth) { - target[prop] = additional[prop]; - seen.push(additional[prop]); - } else { - util.merge(target[prop], additional[prop], depth - 1, seen); - } - } - } + this.subs.push(openSub); + this.subs.push(errorSub); - return target; - }; + return this; +}; - /** - * Merges prototypes from objects - * - * @api public - */ +/** + * Called upon transport open. + * + * @api private + */ - util.mixin = function (ctor, ctor2) { - util.merge(ctor.prototype, ctor2.prototype); - }; +Manager.prototype.onopen = function(){ + debug('open'); - /** - * Shortcut for prototypical and static inheritance. - * - * @api private - */ + // clear old subs + this.cleanup(); - util.inherit = function (ctor, ctor2) { - function f() {}; - f.prototype = ctor2.prototype; - ctor.prototype = new f; - }; + // mark as open + this.readyState = 'open'; + this.emit('open'); - /** - * Checks if the given object is an Array. - * - * io.util.isArray([]); // true - * io.util.isArray({}); // false - * - * @param Object obj - * @api public - */ - - util.isArray = Array.isArray || function (obj) { - return Object.prototype.toString.call(obj) === '[object Array]'; - }; + // add new subs + var socket = this.engine; + this.subs.push(on(socket, 'data', bind(this, 'ondata'))); + this.subs.push(on(this.decoder, 'decoded', bind(this, 'ondecoded'))); + this.subs.push(on(socket, 'error', bind(this, 'onerror'))); + this.subs.push(on(socket, 'close', bind(this, 'onclose'))); +}; - /** - * Intersects values of two arrays into a third - * - * @api public - */ +/** + * Called with data. + * + * @api private + */ - util.intersect = function (arr, arr2) { - var ret = [] - , longest = arr.length > arr2.length ? arr : arr2 - , shortest = arr.length > arr2.length ? arr2 : arr; +Manager.prototype.ondata = function(data){ + this.decoder.add(data); +}; - for (var i = 0, l = shortest.length; i < l; i++) { - if (~util.indexOf(longest, shortest[i])) - ret.push(shortest[i]); - } +/** + * Called when parser fully decodes a packet. + * + * @api private + */ - return ret; - }; +Manager.prototype.ondecoded = function(packet) { + this.emit('packet', packet); +}; - /** - * Array indexOf compatibility. - * - * @see bit.ly/a5Dxa2 - * @api public - */ +/** + * Called upon socket error. + * + * @api private + */ - util.indexOf = function (arr, o, i) { +Manager.prototype.onerror = function(err){ + debug('error', err); + this.emitAll('error', err); +}; - for (var j = arr.length, i = i < 0 ? i + j < 0 ? 0 : i + j : i || 0; - i < j && arr[i] !== o; i++) {} +/** + * Creates a new socket for the given `nsp`. + * + * @return {Socket} + * @api public + */ - return j <= i ? -1 : i; - }; +Manager.prototype.socket = function(nsp){ + var socket = this.nsps[nsp]; + if (!socket) { + socket = new Socket(this, nsp); + this.nsps[nsp] = socket; + var self = this; + socket.on('connect', function(){ + self.connected++; + }); + } + return socket; +}; - /** - * Converts enumerables to array. - * - * @api public - */ +/** + * Called upon a socket close. + * + * @param {Socket} socket + */ - util.toArray = function (enu) { - var arr = []; +Manager.prototype.destroy = function(socket){ + --this.connected || this.close(); +}; - for (var i = 0, l = enu.length; i < l; i++) - arr.push(enu[i]); +/** + * Writes a packet. + * + * @param {Object} packet + * @api private + */ - return arr; - }; +Manager.prototype.packet = function(packet){ + debug('writing packet %j', packet); + var self = this; - /** - * UA / engines detection namespace. - * - * @namespace - */ + if (!self.encoding) { + // encode, then write to engine with result + self.encoding = true; + this.encoder.encode(packet, function(encodedPackets) { + for (var i = 0; i < encodedPackets.length; i++) { + self.engine.write(encodedPackets[i]); + } + self.encoding = false; + self.processPacketQueue(); + }); + } else { // add packet to the queue + self.packetBuffer.push(packet); + } +}; - util.ua = {}; +/** + * If packet buffer is non-empty, begins encoding the + * next packet in line. + * + * @api private + */ - /** - * Whether the UA supports CORS for XHR. - * - * @api public - */ +Manager.prototype.processPacketQueue = function() { + if (this.packetBuffer.length > 0 && !this.encoding) { + var pack = this.packetBuffer.shift(); + this.packet(pack); + } +}; - util.ua.hasCORS = 'undefined' != typeof XMLHttpRequest && (function () { - try { - var a = new XMLHttpRequest(); - } catch (e) { - return false; - } +/** + * Clean up transport subscriptions and packet buffer. + * + * @api private + */ - return a.withCredentials != undefined; - })(); +Manager.prototype.cleanup = function(){ + var sub; + while (sub = this.subs.shift()) sub.destroy(); - /** - * Detect webkit. - * - * @api public - */ + this.packetBuffer = []; + this.encoding = false; - util.ua.webkit = 'undefined' != typeof navigator - && /webkit/i.test(navigator.userAgent); + this.decoder.destroy(); +}; - /** - * Detect iPad/iPhone/iPod. - * - * @api public - */ +/** + * Close the current socket. + * + * @api private + */ - util.ua.iDevice = 'undefined' != typeof navigator - && /iPad|iPhone|iPod/i.test(navigator.userAgent); +Manager.prototype.close = +Manager.prototype.disconnect = function(){ + this.skipReconnect = true; + this.engine.close(); +}; -})('undefined' != typeof io ? io : module.exports, this); /** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed + * Called upon engine close. + * + * @api private */ -(function (exports, io) { - - /** - * Expose constructor. - */ +Manager.prototype.onclose = function(reason){ + debug('close'); + this.cleanup(); + this.readyState = 'closed'; + this.emit('close', reason); + if (this._reconnection && !this.skipReconnect) { + this.reconnect(); + } +}; - exports.EventEmitter = EventEmitter; +/** + * Attempt a reconnection. + * + * @api private + */ - /** - * Event emitter constructor. - * - * @api public. - */ +Manager.prototype.reconnect = function(){ + if (this.reconnecting) return this; - function EventEmitter () {}; + var self = this; + this.attempts++; - /** - * Adds a listener - * - * @api public - */ + if (this.attempts > this._reconnectionAttempts) { + debug('reconnect failed'); + this.emitAll('reconnect_failed'); + this.reconnecting = false; + } else { + var delay = this.attempts * this.reconnectionDelay(); + delay = Math.min(delay, this.reconnectionDelayMax()); + debug('will wait %dms before reconnect attempt', delay); - EventEmitter.prototype.on = function (name, fn) { - if (!this.$events) { - this.$events = {}; - } + this.reconnecting = true; + var timer = setTimeout(function(){ + debug('attempting reconnect'); + self.emitAll('reconnect_attempt', self.attempts); + self.emitAll('reconnecting', self.attempts); + self.open(function(err){ + if (err) { + debug('reconnect attempt error'); + self.reconnecting = false; + self.reconnect(); + self.emitAll('reconnect_error', err.data); + } else { + debug('reconnect success'); + self.onreconnect(); + } + }); + }, delay); - if (!this.$events[name]) { - this.$events[name] = fn; - } else if (io.util.isArray(this.$events[name])) { - this.$events[name].push(fn); - } else { - this.$events[name] = [this.$events[name], fn]; - } + this.subs.push({ + destroy: function(){ + clearTimeout(timer); + } + }); + } +}; - return this; - }; +/** + * Called upon successful reconnect. + * + * @api private + */ - EventEmitter.prototype.addListener = EventEmitter.prototype.on; +Manager.prototype.onreconnect = function(){ + var attempt = this.attempts; + this.attempts = 0; + this.reconnecting = false; + this.emitAll('reconnect', attempt); +}; - /** - * Adds a volatile listener. - * - * @api public - */ +},{"./on":4,"./socket":5,"./url":6,"component-bind":7,"component-emitter":8,"debug":9,"engine.io-client":11,"object-component":37,"socket.io-parser":40}],4:[function(require,module,exports){ - EventEmitter.prototype.once = function (name, fn) { - var self = this; +/** + * Module exports. + */ - function on () { - self.removeListener(name, on); - fn.apply(this, arguments); - }; +module.exports = on; - on.listener = fn; - this.on(name, on); +/** + * Helper for subscriptions. + * + * @param {Object|EventEmitter} obj with `Emitter` mixin or `EventEmitter` + * @param {String} event name + * @param {Function} callback + * @api public + */ - return this; +function on(obj, ev, fn) { + obj.on(ev, fn); + return { + destroy: function(){ + obj.removeListener(ev, fn); + } }; +} - /** - * Removes a listener. - * - * @api public - */ +},{}],5:[function(require,module,exports){ - EventEmitter.prototype.removeListener = function (name, fn) { - if (this.$events && this.$events[name]) { - var list = this.$events[name]; +/** + * Module dependencies. + */ - if (io.util.isArray(list)) { - var pos = -1; +var parser = require('socket.io-parser'); +var Emitter = require('component-emitter'); +var toArray = require('to-array'); +var on = require('./on'); +var bind = require('component-bind'); +var debug = require('debug')('socket.io-client:socket'); +var hasBin = require('has-binary-data'); +var indexOf = require('indexof'); - for (var i = 0, l = list.length; i < l; i++) { - if (list[i] === fn || (list[i].listener && list[i].listener === fn)) { - pos = i; - break; - } - } +/** + * Module exports. + */ - if (pos < 0) { - return this; - } +module.exports = exports = Socket; - list.splice(pos, 1); +/** + * Internal events (blacklisted). + * These events can't be emitted by the user. + * + * @api private + */ - if (!list.length) { - delete this.$events[name]; - } - } else if (list === fn || (list.listener && list.listener === fn)) { - delete this.$events[name]; - } - } +var events = { + connect: 1, + connect_error: 1, + connect_timeout: 1, + disconnect: 1, + error: 1, + reconnect: 1, + reconnect_attempt: 1, + reconnect_failed: 1, + reconnect_error: 1, + reconnecting: 1 +}; - return this; - }; +/** + * Shortcut to `Emitter#emit`. + */ - /** - * Removes all listeners for an event. - * - * @api public - */ +var emit = Emitter.prototype.emit; - EventEmitter.prototype.removeAllListeners = function (name) { - if (name === undefined) { - this.$events = {}; - return this; - } +/** + * `Socket` constructor. + * + * @api public + */ - if (this.$events && this.$events[name]) { - this.$events[name] = null; - } +function Socket(io, nsp){ + this.io = io; + this.nsp = nsp; + this.json = this; // compat + this.ids = 0; + this.acks = {}; + this.open(); + this.receiveBuffer = []; + this.sendBuffer = []; + this.connected = false; + this.disconnected = true; + this.subEvents(); +} - return this; - }; +/** + * Mix in `Emitter`. + */ - /** - * Gets all listeners for a certain event. - * - * @api publci - */ +Emitter(Socket.prototype); - EventEmitter.prototype.listeners = function (name) { - if (!this.$events) { - this.$events = {}; - } +/** + * Subscribe to open, close and packet events + * + * @api private + */ - if (!this.$events[name]) { - this.$events[name] = []; - } +Socket.prototype.subEvents = function() { + var io = this.io; + this.subs = [ + on(io, 'open', bind(this, 'onopen')), + on(io, 'packet', bind(this, 'onpacket')), + on(io, 'close', bind(this, 'onclose')) + ]; +}; - if (!io.util.isArray(this.$events[name])) { - this.$events[name] = [this.$events[name]]; - } +/** + * Called upon engine `open`. + * + * @api private + */ - return this.$events[name]; - }; +Socket.prototype.open = +Socket.prototype.connect = function(){ + if (this.connected) return this; - /** - * Emits an event. - * - * @api public - */ + this.io.open(); // ensure open + if ('open' == this.io.readyState) this.onopen(); + return this; +}; - EventEmitter.prototype.emit = function (name) { - if (!this.$events) { - return false; - } +/** + * Sends a `message` event. + * + * @return {Socket} self + * @api public + */ - var handler = this.$events[name]; +Socket.prototype.send = function(){ + var args = toArray(arguments); + args.unshift('message'); + this.emit.apply(this, args); + return this; +}; - if (!handler) { - return false; - } +/** + * Override `emit`. + * If the event is in `events`, it's emitted normally. + * + * @param {String} event name + * @return {Socket} self + * @api public + */ - var args = Array.prototype.slice.call(arguments, 1); +Socket.prototype.emit = function(ev){ + if (events.hasOwnProperty(ev)) { + emit.apply(this, arguments); + return this; + } - if ('function' == typeof handler) { - handler.apply(this, args); - } else if (io.util.isArray(handler)) { - var listeners = handler.slice(); + var args = toArray(arguments); + var parserType = parser.EVENT; // default + if (hasBin(args)) { parserType = parser.BINARY_EVENT; } // binary + var packet = { type: parserType, data: args }; - for (var i = 0, l = listeners.length; i < l; i++) { - listeners[i].apply(this, args); - } - } else { - return false; - } + // event ack callback + if ('function' == typeof args[args.length - 1]) { + debug('emitting packet with ack id %d', this.ids); + this.acks[this.ids] = args.pop(); + packet.id = this.ids++; + } - return true; - }; + if (this.connected) { + this.packet(packet); + } else { + this.sendBuffer.push(packet); + } -})( - 'undefined' != typeof io ? io : module.exports - , 'undefined' != typeof io ? io : module.parent.exports -); + return this; +}; /** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed + * Sends a packet. + * + * @param {Object} packet + * @api private */ +Socket.prototype.packet = function(packet){ + packet.nsp = this.nsp; + this.io.packet(packet); +}; + /** - * Based on JSON2 (http://www.JSON.org/js.html). + * "Opens" the socket. + * + * @api private */ -(function (exports, nativeJSON) { - "use strict"; +Socket.prototype.onopen = function(){ + debug('transport is open - connecting'); - // use native JSON if it's available - if (nativeJSON && nativeJSON.parse){ - return exports.JSON = { - parse: nativeJSON.parse - , stringify: nativeJSON.stringify - }; + // write connect packet if necessary + if ('/' != this.nsp) { + this.packet({ type: parser.CONNECT }); } +}; - var JSON = exports.JSON = {}; +/** + * Called upon engine `close`. + * + * @param {String} reason + * @api private + */ - function f(n) { - // Format integers to have at least two digits. - return n < 10 ? '0' + n : n; - } +Socket.prototype.onclose = function(reason){ + debug('close (%s)', reason); + this.connected = false; + this.disconnected = true; + this.emit('disconnect', reason); +}; - function date(d, key) { - return isFinite(d.valueOf()) ? - d.getUTCFullYear() + '-' + - f(d.getUTCMonth() + 1) + '-' + - f(d.getUTCDate()) + 'T' + - f(d.getUTCHours()) + ':' + - f(d.getUTCMinutes()) + ':' + - f(d.getUTCSeconds()) + 'Z' : null; - }; +/** + * Called with socket packet. + * + * @param {Object} packet + * @api private + */ - var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, - escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, - gap, - indent, - meta = { // table of character substitutions - '\b': '\\b', - '\t': '\\t', - '\n': '\\n', - '\f': '\\f', - '\r': '\\r', - '"' : '\\"', - '\\': '\\\\' - }, - rep; +Socket.prototype.onpacket = function(packet){ + if (packet.nsp != this.nsp) return; + switch (packet.type) { + case parser.CONNECT: + this.onconnect(); + break; - function quote(string) { + case parser.EVENT: + this.onevent(packet); + break; -// If the string contains no control characters, no quote characters, and no -// backslash characters, then we can safely slap some quotes around it. -// Otherwise we must also replace the offending characters with safe escape -// sequences. + case parser.BINARY_EVENT: + this.onevent(packet); + break; - escapable.lastIndex = 0; - return escapable.test(string) ? '"' + string.replace(escapable, function (a) { - var c = meta[a]; - return typeof c === 'string' ? c : - '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }) + '"' : '"' + string + '"'; - } + case parser.ACK: + this.onack(packet); + break; + case parser.BINARY_ACK: + this.onack(packet); + break; - function str(key, holder) { + case parser.DISCONNECT: + this.ondisconnect(); + break; -// Produce a string from holder[key]. + case parser.ERROR: + this.emit('error', packet.data); + break; + } +}; - var i, // The loop counter. - k, // The member key. - v, // The member value. - length, - mind = gap, - partial, - value = holder[key]; +/** + * Called upon a server event. + * + * @param {Object} packet + * @api private + */ -// If the value has a toJSON method, call it to obtain a replacement value. +Socket.prototype.onevent = function(packet){ + var args = packet.data || []; + debug('emitting event %j', args); - if (value instanceof Date) { - value = date(key); - } + if (null != packet.id) { + debug('attaching ack callback to event'); + args.push(this.ack(packet.id)); + } -// If we were called with a replacer function, then call the replacer to -// obtain a replacement value. + if (this.connected) { + emit.apply(this, args); + } else { + this.receiveBuffer.push(args); + } +}; - if (typeof rep === 'function') { - value = rep.call(holder, key, value); - } +/** + * Produces an ack callback to emit with an event. + * + * @api private + */ -// What happens next depends on the value's type. +Socket.prototype.ack = function(id){ + var self = this; + var sent = false; + return function(){ + // prevent double callbacks + if (sent) return; + sent = true; + var args = toArray(arguments); + debug('sending ack %j', args); + + var type = hasBin(args) ? parser.BINARY_ACK : parser.ACK; + self.packet({ + type: type, + id: id, + data: args + }); + }; +}; - switch (typeof value) { - case 'string': - return quote(value); +/** + * Called upon a server acknowlegement. + * + * @param {Object} packet + * @api private + */ - case 'number': +Socket.prototype.onack = function(packet){ + debug('calling ack %s with %j', packet.id, packet.data); + var fn = this.acks[packet.id]; + fn.apply(this, packet.data); + delete this.acks[packet.id]; +}; -// JSON numbers must be finite. Encode non-finite numbers as null. +/** + * Called upon server connect. + * + * @api private + */ - return isFinite(value) ? String(value) : 'null'; +Socket.prototype.onconnect = function(){ + this.connected = true; + this.disconnected = false; + this.emit('connect'); + this.emitBuffered(); +}; - case 'boolean': - case 'null': +/** + * Emit buffered events (received and emitted). + * + * @api private + */ -// If the value is a boolean or null, convert it to a string. Note: -// typeof null does not produce 'null'. The case is included here in -// the remote chance that this gets fixed someday. +Socket.prototype.emitBuffered = function(){ + var i; + for (i = 0; i < this.receiveBuffer.length; i++) { + emit.apply(this, this.receiveBuffer[i]); + } + this.receiveBuffer = []; - return String(value); + for (i = 0; i < this.sendBuffer.length; i++) { + this.packet(this.sendBuffer[i]); + } + this.sendBuffer = []; +}; -// If the type is 'object', we might be dealing with an object or an array or -// null. +/** + * Called upon server disconnect. + * + * @api private + */ - case 'object': +Socket.prototype.ondisconnect = function(){ + debug('server disconnect (%s)', this.nsp); + this.destroy(); + this.onclose('io server disconnect'); +}; -// Due to a specification blunder in ECMAScript, typeof null is 'object', -// so watch out for that case. +/** + * Called upon forced client/server side disconnections, + * this method ensures the manager stops tracking us and + * that reconnections don't get triggered for this. + * + * @api private. + */ - if (!value) { - return 'null'; - } +Socket.prototype.destroy = function(){ + // clean subscriptions to avoid reconnections + for (var i = 0; i < this.subs.length; i++) { + this.subs[i].destroy(); + } -// Make an array to hold the partial results of stringifying this object value. + this.io.destroy(this); +}; - gap += indent; - partial = []; +/** + * Disconnects the socket manually. + * + * @return {Socket} self + * @api public + */ -// Is the value an array? +Socket.prototype.close = +Socket.prototype.disconnect = function(){ + if (!this.connected) return this; - if (Object.prototype.toString.apply(value) === '[object Array]') { + debug('performing disconnect (%s)', this.nsp); + this.packet({ type: parser.DISCONNECT }); -// The value is an array. Stringify every element. Use null as a placeholder -// for non-JSON values. + // remove socket from pool + this.destroy(); - length = value.length; - for (i = 0; i < length; i += 1) { - partial[i] = str(i, value) || 'null'; - } + // fire events + this.onclose('io client disconnect'); + return this; +}; -// Join all of the elements together, separated with commas, and wrap them in -// brackets. +},{"./on":4,"component-bind":7,"component-emitter":8,"debug":9,"has-binary-data":32,"indexof":36,"socket.io-parser":40,"to-array":43}],6:[function(require,module,exports){ +var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}; +/** + * Module dependencies. + */ - v = partial.length === 0 ? '[]' : gap ? - '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : - '[' + partial.join(',') + ']'; - gap = mind; - return v; - } +var parseuri = require('parseuri'); +var debug = require('debug')('socket.io-client:url'); -// If the replacer is an array, use it to select the members to be stringified. +/** + * Module exports. + */ - if (rep && typeof rep === 'object') { - length = rep.length; - for (i = 0; i < length; i += 1) { - if (typeof rep[i] === 'string') { - k = rep[i]; - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - } else { +module.exports = url; -// Otherwise, iterate through all of the keys in the object. +/** + * URL parser. + * + * @param {String} url + * @param {Object} An object meant to mimic window.location. + * Defaults to window.location. + * @api public + */ - for (k in value) { - if (Object.prototype.hasOwnProperty.call(value, k)) { - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - } +function url(uri, loc){ + var obj = uri; -// Join all of the member texts together, separated with commas, -// and wrap them in braces. + // default to window.location + var loc = loc || global.location; + if (null == uri) uri = loc.protocol + '//' + loc.hostname; - v = partial.length === 0 ? '{}' : gap ? - '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : - '{' + partial.join(',') + '}'; - gap = mind; - return v; + // relative path support + if ('string' == typeof uri) { + if ('/' == uri.charAt(0)) { + if ('undefined' != typeof loc) { + uri = loc.hostname + uri; } - } - -// If the JSON object does not yet have a stringify method, give it one. - - JSON.stringify = function (value, replacer, space) { + } -// The stringify method takes a value and an optional replacer, and an optional -// space parameter, and returns a JSON text. The replacer can be a function -// that can replace values, or an array of strings that will select the keys. -// A default replacer method can be provided. Use of the space parameter can -// produce text that is more easily readable. + if (!/^(https?|wss?):\/\//.test(uri)) { + debug('protocol-less url %s', uri); + if ('undefined' != typeof loc) { + uri = loc.protocol + '//' + uri; + } else { + uri = 'https://' + uri; + } + } - var i; - gap = ''; - indent = ''; + // parse + debug('parse %s', uri); + obj = parseuri(uri); + } -// If the space parameter is a number, make an indent string containing that -// many spaces. + // make sure we treat `localhost:80` and `localhost` equally + if (!obj.port) { + if (/^(http|ws)$/.test(obj.protocol)) { + obj.port = '80'; + } + else if (/^(http|ws)s$/.test(obj.protocol)) { + obj.port = '443'; + } + } - if (typeof space === 'number') { - for (i = 0; i < space; i += 1) { - indent += ' '; - } + obj.path = obj.path || '/'; -// If the space parameter is a string, it will be used as the indent string. + // define unique id + obj.id = obj.protocol + '://' + obj.host + ':' + obj.port; + // define href + obj.href = obj.protocol + '://' + obj.host + (loc && loc.port == obj.port ? '' : (':' + obj.port)); - } else if (typeof space === 'string') { - indent = space; - } + return obj; +} -// If there is a replacer, it must be a function or an array. -// Otherwise, throw an error. +},{"debug":9,"parseuri":38}],7:[function(require,module,exports){ +/** + * Slice reference. + */ - rep = replacer; - if (replacer && typeof replacer !== 'function' && - (typeof replacer !== 'object' || - typeof replacer.length !== 'number')) { - throw new Error('JSON.stringify'); - } +var slice = [].slice; -// Make a fake root object containing our value under the key of ''. -// Return the result of stringifying the value. +/** + * Bind `obj` to `fn`. + * + * @param {Object} obj + * @param {Function|String} fn or string + * @return {Function} + * @api public + */ - return str('', {'': value}); - }; +module.exports = function(obj, fn){ + if ('string' == typeof fn) fn = obj[fn]; + if ('function' != typeof fn) throw new Error('bind() requires a function'); + var args = slice.call(arguments, 2); + return function(){ + return fn.apply(obj, args.concat(slice.call(arguments))); + } +}; -// If the JSON object does not yet have a parse method, give it one. +},{}],8:[function(require,module,exports){ - JSON.parse = function (text, reviver) { - // The parse method takes a text and an optional reviver function, and returns - // a JavaScript value if the text is a valid JSON text. +/** + * Expose `Emitter`. + */ - var j; +module.exports = Emitter; - function walk(holder, key) { +/** + * Initialize a new `Emitter`. + * + * @api public + */ - // The walk method is used to recursively walk the resulting structure so - // that modifications can be made. +function Emitter(obj) { + if (obj) return mixin(obj); +}; - var k, v, value = holder[key]; - if (value && typeof value === 'object') { - for (k in value) { - if (Object.prototype.hasOwnProperty.call(value, k)) { - v = walk(value, k); - if (v !== undefined) { - value[k] = v; - } else { - delete value[k]; - } - } - } - } - return reviver.call(holder, key, value); - } +/** + * Mixin the emitter properties. + * + * @param {Object} obj + * @return {Object} + * @api private + */ +function mixin(obj) { + for (var key in Emitter.prototype) { + obj[key] = Emitter.prototype[key]; + } + return obj; +} - // Parsing happens in four stages. In the first stage, we replace certain - // Unicode characters with escape sequences. JavaScript handles many characters - // incorrectly, either silently deleting them, or treating them as line endings. +/** + * Listen on the given `event` with `fn`. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ - text = String(text); - cx.lastIndex = 0; - if (cx.test(text)) { - text = text.replace(cx, function (a) { - return '\\u' + - ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }); - } +Emitter.prototype.on = +Emitter.prototype.addEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; + (this._callbacks[event] = this._callbacks[event] || []) + .push(fn); + return this; +}; - // In the second stage, we run the text against regular expressions that look - // for non-JSON patterns. We are especially concerned with '()' and 'new' - // because they can cause invocation, and '=' because it can cause mutation. - // But just to be safe, we want to reject all unexpected forms. +/** + * Adds an `event` listener that will be invoked a single + * time then automatically removed. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ - // We split the second stage into 4 regexp operations in order to work around - // crippling inefficiencies in IE's and Safari's regexp engines. First we - // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we - // replace all simple value tokens with ']' characters. Third, we delete all - // open brackets that follow a colon or comma or that begin the text. Finally, - // we look to see that the remaining characters are only whitespace or ']' or - // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. +Emitter.prototype.once = function(event, fn){ + var self = this; + this._callbacks = this._callbacks || {}; - if (/^[\],:{}\s]*$/ - .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') - .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') - .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + function on() { + self.off(event, on); + fn.apply(this, arguments); + } - // In the third stage we use the eval function to compile the text into a - // JavaScript structure. The '{' operator is subject to a syntactic ambiguity - // in JavaScript: it can begin a block or an object literal. We wrap the text - // in parens to eliminate the ambiguity. + on.fn = fn; + this.on(event, on); + return this; +}; - j = eval('(' + text + ')'); +/** + * Remove the given callback for `event` or all + * registered callbacks. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ - // In the optional fourth stage, we recursively walk the new structure, passing - // each name/value pair to a reviver function for possible transformation. +Emitter.prototype.off = +Emitter.prototype.removeListener = +Emitter.prototype.removeAllListeners = +Emitter.prototype.removeEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; - return typeof reviver === 'function' ? - walk({'': j}, '') : j; - } + // all + if (0 == arguments.length) { + this._callbacks = {}; + return this; + } - // If the text is not JSON parseable, then a SyntaxError is thrown. + // specific event + var callbacks = this._callbacks[event]; + if (!callbacks) return this; - throw new SyntaxError('JSON.parse'); - }; + // remove all handlers + if (1 == arguments.length) { + delete this._callbacks[event]; + return this; + } -})( - 'undefined' != typeof io ? io : module.exports - , typeof JSON !== 'undefined' ? JSON : undefined -); + // remove specific handler + var cb; + for (var i = 0; i < callbacks.length; i++) { + cb = callbacks[i]; + if (cb === fn || cb.fn === fn) { + callbacks.splice(i, 1); + break; + } + } + return this; +}; /** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed + * Emit `event` with the given args. + * + * @param {String} event + * @param {Mixed} ... + * @return {Emitter} */ -(function (exports, io) { +Emitter.prototype.emit = function(event){ + this._callbacks = this._callbacks || {}; + var args = [].slice.call(arguments, 1) + , callbacks = this._callbacks[event]; - /** - * Parser namespace. - * - * @namespace - */ - - var parser = exports.parser = {}; + if (callbacks) { + callbacks = callbacks.slice(0); + for (var i = 0, len = callbacks.length; i < len; ++i) { + callbacks[i].apply(this, args); + } + } - /** - * Packet types. - */ + return this; +}; - var packets = parser.packets = [ - 'disconnect' - , 'connect' - , 'heartbeat' - , 'message' - , 'json' - , 'event' - , 'ack' - , 'error' - , 'noop' - ]; +/** + * Return array of callbacks for `event`. + * + * @param {String} event + * @return {Array} + * @api public + */ - /** - * Errors reasons. - */ +Emitter.prototype.listeners = function(event){ + this._callbacks = this._callbacks || {}; + return this._callbacks[event] || []; +}; - var reasons = parser.reasons = [ - 'transport not supported' - , 'client not handshaken' - , 'unauthorized' - ]; +/** + * Check if this emitter has `event` handlers. + * + * @param {String} event + * @return {Boolean} + * @api public + */ - /** - * Errors advice. - */ +Emitter.prototype.hasListeners = function(event){ + return !! this.listeners(event).length; +}; - var advice = parser.advice = [ - 'reconnect' - ]; +},{}],9:[function(require,module,exports){ - /** - * Shortcuts. - */ +/** + * Expose `debug()` as the module. + */ - var JSON = io.JSON - , indexOf = io.util.indexOf; +module.exports = debug; - /** - * Encodes a packet. - * - * @api private - */ +/** + * Create a debugger with the given `name`. + * + * @param {String} name + * @return {Type} + * @api public + */ - parser.encodePacket = function (packet) { - var type = indexOf(packets, packet.type) - , id = packet.id || '' - , endpoint = packet.endpoint || '' - , ack = packet.ack - , data = null; +function debug(name) { + if (!debug.enabled(name)) return function(){}; - switch (packet.type) { - case 'error': - var reason = packet.reason ? indexOf(reasons, packet.reason) : '' - , adv = packet.advice ? indexOf(advice, packet.advice) : ''; + return function(fmt){ + fmt = coerce(fmt); - if (reason !== '' || adv !== '') - data = reason + (adv !== '' ? ('+' + adv) : ''); + var curr = new Date; + var ms = curr - (debug[name] || curr); + debug[name] = curr; - break; + fmt = name + + ' ' + + fmt + + ' +' + debug.humanize(ms); - case 'message': - if (packet.data !== '') - data = packet.data; - break; + // This hackery is required for IE8 + // where `console.log` doesn't have 'apply' + window.console + && console.log + && Function.prototype.apply.call(console.log, console, arguments); + } +} - case 'event': - var ev = { name: packet.name }; +/** + * The currently active debug mode names. + */ - if (packet.args && packet.args.length) { - ev.args = packet.args; - } +debug.names = []; +debug.skips = []; - data = JSON.stringify(ev); - break; +/** + * Enables a debug mode by name. This can include modes + * separated by a colon and wildcards. + * + * @param {String} name + * @api public + */ - case 'json': - data = JSON.stringify(packet.data); - break; +debug.enable = function(name) { + try { + localStorage.debug = name; + } catch(e){} - case 'connect': - if (packet.qs) - data = packet.qs; - break; + var split = (name || '').split(/[\s,]+/) + , len = split.length; - case 'ack': - data = packet.ackId - + (packet.args && packet.args.length - ? '+' + JSON.stringify(packet.args) : ''); - break; + for (var i = 0; i < len; i++) { + name = split[i].replace('*', '.*?'); + if (name[0] === '-') { + debug.skips.push(new RegExp('^' + name.substr(1) + '$')); + } + else { + debug.names.push(new RegExp('^' + name + '$')); } + } +}; - // construct packet with required fragments - var encoded = [ - type - , id + (ack == 'data' ? '+' : '') - , endpoint - ]; +/** + * Disable debug output. + * + * @api public + */ - // data fragment is optional - if (data !== null && data !== undefined) - encoded.push(data); +debug.disable = function(){ + debug.enable(''); +}; - return encoded.join(':'); - }; +/** + * Humanize the given `ms`. + * + * @param {Number} m + * @return {String} + * @api private + */ - /** - * Encodes multiple messages (payload). - * - * @param {Array} messages - * @api private - */ +debug.humanize = function(ms) { + var sec = 1000 + , min = 60 * 1000 + , hour = 60 * min; - parser.encodePayload = function (packets) { - var decoded = ''; + if (ms >= hour) return (ms / hour).toFixed(1) + 'h'; + if (ms >= min) return (ms / min).toFixed(1) + 'm'; + if (ms >= sec) return (ms / sec | 0) + 's'; + return ms + 'ms'; +}; - if (packets.length == 1) - return packets[0]; +/** + * Returns true if the given mode name is enabled, false otherwise. + * + * @param {String} name + * @return {Boolean} + * @api public + */ - for (var i = 0, l = packets.length; i < l; i++) { - var packet = packets[i]; - decoded += '\ufffd' + packet.length + '\ufffd' + packets[i]; +debug.enabled = function(name) { + for (var i = 0, len = debug.skips.length; i < len; i++) { + if (debug.skips[i].test(name)) { + return false; } + } + for (var i = 0, len = debug.names.length; i < len; i++) { + if (debug.names[i].test(name)) { + return true; + } + } + return false; +}; - return decoded; - }; +/** + * Coerce `val`. + */ - /** - * Decodes a packet - * - * @api private - */ +function coerce(val) { + if (val instanceof Error) return val.stack || val.message; + return val; +} - var regexp = /([^:]+):([0-9]+)?(\+)?:([^:]+)?:?([\s\S]*)?/; +// persist - parser.decodePacket = function (data) { - var pieces = data.match(regexp); +try { + if (window.localStorage) debug.enable(localStorage.debug); +} catch(e){} - if (!pieces) return {}; +},{}],10:[function(require,module,exports){ - var id = pieces[2] || '' - , data = pieces[5] || '' - , packet = { - type: packets[pieces[1]] - , endpoint: pieces[4] || '' - }; +/** + * Module dependencies. + */ - // whether we need to acknowledge the packet - if (id) { - packet.id = id; - if (pieces[3]) - packet.ack = 'data'; - else - packet.ack = true; - } +var index = require('indexof'); - // handle different packet types - switch (packet.type) { - case 'error': - var pieces = data.split('+'); - packet.reason = reasons[pieces[0]] || ''; - packet.advice = advice[pieces[1]] || ''; - break; +/** + * Expose `Emitter`. + */ - case 'message': - packet.data = data || ''; - break; +module.exports = Emitter; - case 'event': - try { - var opts = JSON.parse(data); - packet.name = opts.name; - packet.args = opts.args; - } catch (e) { } +/** + * Initialize a new `Emitter`. + * + * @api public + */ - packet.args = packet.args || []; - break; +function Emitter(obj) { + if (obj) return mixin(obj); +}; - case 'json': - try { - packet.data = JSON.parse(data); - } catch (e) { } - break; +/** + * Mixin the emitter properties. + * + * @param {Object} obj + * @return {Object} + * @api private + */ - case 'connect': - packet.qs = data || ''; - break; +function mixin(obj) { + for (var key in Emitter.prototype) { + obj[key] = Emitter.prototype[key]; + } + return obj; +} - case 'ack': - var pieces = data.match(/^([0-9]+)(\+)?(.*)/); - if (pieces) { - packet.ackId = pieces[1]; - packet.args = []; +/** + * Listen on the given `event` with `fn`. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ - if (pieces[3]) { - try { - packet.args = pieces[3] ? JSON.parse(pieces[3]) : []; - } catch (e) { } - } - } - break; +Emitter.prototype.on = function(event, fn){ + this._callbacks = this._callbacks || {}; + (this._callbacks[event] = this._callbacks[event] || []) + .push(fn); + return this; +}; - case 'disconnect': - case 'heartbeat': - break; - }; +/** + * Adds an `event` listener that will be invoked a single + * time then automatically removed. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ - return packet; - }; +Emitter.prototype.once = function(event, fn){ + var self = this; + this._callbacks = this._callbacks || {}; - /** - * Decodes data payload. Detects multiple messages - * - * @return {Array} messages - * @api public - */ - - parser.decodePayload = function (data) { - // IE doesn't like data[i] for unicode chars, charAt works fine - if (data.charAt(0) == '\ufffd') { - var ret = []; - - for (var i = 1, length = ''; i < data.length; i++) { - if (data.charAt(i) == '\ufffd') { - ret.push(parser.decodePacket(data.substr(i + 1).substr(0, length))); - i += Number(length) + 1; - length = ''; - } else { - length += data.charAt(i); - } - } + function on() { + self.off(event, on); + fn.apply(this, arguments); + } - return ret; - } else { - return [parser.decodePacket(data)]; - } - }; + fn._off = on; + this.on(event, on); + return this; +}; -})( - 'undefined' != typeof io ? io : module.exports - , 'undefined' != typeof io ? io : module.parent.exports -); /** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed + * Remove the given callback for `event` or all + * registered callbacks. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public */ -(function (exports, io) { +Emitter.prototype.off = +Emitter.prototype.removeListener = +Emitter.prototype.removeAllListeners = function(event, fn){ + this._callbacks = this._callbacks || {}; - /** - * Expose constructor. - */ + // all + if (0 == arguments.length) { + this._callbacks = {}; + return this; + } - exports.Transport = Transport; + // specific event + var callbacks = this._callbacks[event]; + if (!callbacks) return this; - /** - * This is the transport template for all supported transport methods. - * - * @constructor - * @api public - */ + // remove all handlers + if (1 == arguments.length) { + delete this._callbacks[event]; + return this; + } - function Transport (socket, sessid) { - this.socket = socket; - this.sessid = sessid; - }; + // remove specific handler + var i = index(callbacks, fn._off || fn); + if (~i) callbacks.splice(i, 1); + return this; +}; - /** - * Apply EventEmitter mixin. - */ +/** + * Emit `event` with the given args. + * + * @param {String} event + * @param {Mixed} ... + * @return {Emitter} + */ - io.util.mixin(Transport, io.EventEmitter); +Emitter.prototype.emit = function(event){ + this._callbacks = this._callbacks || {}; + var args = [].slice.call(arguments, 1) + , callbacks = this._callbacks[event]; + if (callbacks) { + callbacks = callbacks.slice(0); + for (var i = 0, len = callbacks.length; i < len; ++i) { + callbacks[i].apply(this, args); + } + } - /** - * Indicates whether heartbeats is enabled for this transport - * - * @api private - */ + return this; +}; - Transport.prototype.heartbeats = function () { - return true; - }; +/** + * Return array of callbacks for `event`. + * + * @param {String} event + * @return {Array} + * @api public + */ - /** - * Handles the response from the server. When a new response is received - * it will automatically update the timeout, decode the message and - * forwards the response to the onMessage function for further processing. - * - * @param {String} data Response from the server. - * @api private - */ +Emitter.prototype.listeners = function(event){ + this._callbacks = this._callbacks || {}; + return this._callbacks[event] || []; +}; - Transport.prototype.onData = function (data) { - this.clearCloseTimeout(); +/** + * Check if this emitter has `event` handlers. + * + * @param {String} event + * @return {Boolean} + * @api public + */ - // If the connection in currently open (or in a reopening state) reset the close - // timeout since we have just received data. This check is necessary so - // that we don't reset the timeout on an explicitly disconnected connection. - if (this.socket.connected || this.socket.connecting || this.socket.reconnecting) { - this.setCloseTimeout(); - } +Emitter.prototype.hasListeners = function(event){ + return !! this.listeners(event).length; +}; - if (data !== '') { - // todo: we should only do decodePayload for xhr transports - var msgs = io.parser.decodePayload(data); +},{"indexof":36}],11:[function(require,module,exports){ - if (msgs && msgs.length) { - for (var i = 0, l = msgs.length; i < l; i++) { - this.onPacket(msgs[i]); - } - } - } +module.exports = require('./lib/'); - return this; - }; +},{"./lib/":12}],12:[function(require,module,exports){ - /** - * Handles packets. - * - * @api private - */ +module.exports = require('./socket'); - Transport.prototype.onPacket = function (packet) { - this.socket.setHeartbeatTimeout(); +/** + * Exports parser + * + * @api public + * + */ +module.exports.parser = require('engine.io-parser'); - if (packet.type == 'heartbeat') { - return this.onHeartbeat(); - } +},{"./socket":13,"engine.io-parser":22}],13:[function(require,module,exports){ +var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};/** + * Module dependencies. + */ - if (packet.type == 'connect' && packet.endpoint == '') { - this.onConnect(); - } +var transports = require('./transports'); +var Emitter = require('component-emitter'); +var debug = require('debug')('engine.io-client:socket'); +var index = require('indexof'); +var parser = require('engine.io-parser'); +var parseuri = require('parseuri'); +var parsejson = require('parsejson'); +var parseqs = require('parseqs'); - if (packet.type == 'error' && packet.advice == 'reconnect') { - this.isOpen = false; - } +/** + * Module exports. + */ - this.socket.onPacket(packet); +module.exports = Socket; - return this; - }; +/** + * Noop function. + * + * @api private + */ - /** - * Sets close timeout - * - * @api private - */ +function noop(){} - Transport.prototype.setCloseTimeout = function () { - if (!this.closeTimeout) { - var self = this; +/** + * Socket constructor. + * + * @param {String|Object} uri or options + * @param {Object} options + * @api public + */ - this.closeTimeout = setTimeout(function () { - self.onDisconnect(); - }, this.socket.closeTimeout); - } - }; +function Socket(uri, opts){ + if (!(this instanceof Socket)) return new Socket(uri, opts); - /** - * Called when transport disconnects. - * - * @api private - */ + opts = opts || {}; - Transport.prototype.onDisconnect = function () { - if (this.isOpen) this.close(); - this.clearTimeouts(); - this.socket.onDisconnect(); - return this; - }; + if (uri && 'object' == typeof uri) { + opts = uri; + uri = null; + } - /** - * Called when transport connects - * - * @api private - */ + if (uri) { + uri = parseuri(uri); + opts.host = uri.host; + opts.secure = uri.protocol == 'https' || uri.protocol == 'wss'; + opts.port = uri.port; + if (uri.query) opts.query = uri.query; + } - Transport.prototype.onConnect = function () { - this.socket.onConnect(); - return this; - }; + this.secure = null != opts.secure ? opts.secure : + (global.location && 'https:' == location.protocol); - /** - * Clears close timeout - * - * @api private - */ + if (opts.host) { + var pieces = opts.host.split(':'); + opts.hostname = pieces.shift(); + if (pieces.length) opts.port = pieces.pop(); + } - Transport.prototype.clearCloseTimeout = function () { - if (this.closeTimeout) { - clearTimeout(this.closeTimeout); - this.closeTimeout = null; - } - }; + this.agent = opts.agent || false; + this.hostname = opts.hostname || + (global.location ? location.hostname : 'localhost'); + this.port = opts.port || (global.location && location.port ? + location.port : + (this.secure ? 443 : 80)); + this.query = opts.query || {}; + if ('string' == typeof this.query) this.query = parseqs.decode(this.query); + this.upgrade = false !== opts.upgrade; + this.path = (opts.path || '/engine.io').replace(/\/$/, '') + '/'; + this.forceJSONP = !!opts.forceJSONP; + this.forceBase64 = !!opts.forceBase64; + this.timestampParam = opts.timestampParam || 't'; + this.timestampRequests = opts.timestampRequests; + this.transports = opts.transports || ['polling', 'websocket']; + this.readyState = ''; + this.writeBuffer = []; + this.callbackBuffer = []; + this.policyPort = opts.policyPort || 843; + this.rememberUpgrade = opts.rememberUpgrade || false; + this.open(); + this.binaryType = null; + this.onlyBinaryUpgrades = opts.onlyBinaryUpgrades; +} - /** - * Clear timeouts - * - * @api private - */ +Socket.priorWebsocketSuccess = false; - Transport.prototype.clearTimeouts = function () { - this.clearCloseTimeout(); +/** + * Mix in `Emitter`. + */ - if (this.reopenTimeout) { - clearTimeout(this.reopenTimeout); - } - }; +Emitter(Socket.prototype); - /** - * Sends a packet - * - * @param {Object} packet object. - * @api private - */ +/** + * Protocol version. + * + * @api public + */ - Transport.prototype.packet = function (packet) { - this.send(io.parser.encodePacket(packet)); - }; +Socket.protocol = parser.protocol; // this is an int - /** - * Send the received heartbeat message back to server. So the server - * knows we are still connected. - * - * @param {String} heartbeat Heartbeat response from the server. - * @api private - */ +/** + * Expose deps for legacy compatibility + * and standalone browser access. + */ - Transport.prototype.onHeartbeat = function (heartbeat) { - this.packet({ type: 'heartbeat' }); - }; +Socket.Socket = Socket; +Socket.Transport = require('./transport'); +Socket.transports = require('./transports'); +Socket.parser = require('engine.io-parser'); - /** - * Called when the transport opens. - * - * @api private - */ +/** + * Creates transport of the given type. + * + * @param {String} transport name + * @return {Transport} + * @api private + */ - Transport.prototype.onOpen = function () { - this.isOpen = true; - this.clearCloseTimeout(); - this.socket.onOpen(); - }; +Socket.prototype.createTransport = function (name) { + debug('creating transport "%s"', name); + var query = clone(this.query); + + // append engine.io protocol identifier + query.EIO = parser.protocol; + + // transport name + query.transport = name; + + // session id if we already have one + if (this.id) query.sid = this.id; + + var transport = new transports[name]({ + agent: this.agent, + hostname: this.hostname, + port: this.port, + secure: this.secure, + path: this.path, + query: query, + forceJSONP: this.forceJSONP, + forceBase64: this.forceBase64, + timestampRequests: this.timestampRequests, + timestampParam: this.timestampParam, + policyPort: this.policyPort, + socket: this + }); + + return transport; +}; - /** - * Notifies the base when the connection with the Socket.IO server - * has been disconnected. - * - * @api private - */ +function clone (obj) { + var o = {}; + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + o[i] = obj[i]; + } + } + return o; +} - Transport.prototype.onClose = function () { - var self = this; +/** + * Initializes transport to use and starts probe. + * + * @api private + */ +Socket.prototype.open = function () { + var transport; + if (this.rememberUpgrade && Socket.priorWebsocketSuccess && this.transports.indexOf('websocket') != -1) { + transport = 'websocket'; + } else { + transport = this.transports[0]; + } + this.readyState = 'opening'; + var transport = this.createTransport(transport); + transport.open(); + this.setTransport(transport); +}; - /* FIXME: reopen delay causing a infinit loop - this.reopenTimeout = setTimeout(function () { - self.open(); - }, this.socket.options['reopen delay']);*/ +/** + * Sets the current transport. Disables the existing one (if any). + * + * @api private + */ - this.isOpen = false; - this.socket.onClose(); - this.onDisconnect(); - }; +Socket.prototype.setTransport = function(transport){ + debug('setting transport %s', transport.name); + var self = this; - /** - * Generates a connection url based on the Socket.IO URL Protocol. - * See for more details. - * - * @returns {String} Connection url - * @api private - */ - - Transport.prototype.prepareUrl = function () { - var options = this.socket.options; - - return this.scheme() + '://' - + options.host + ':' + options.port + '/' - + options.resource + '/' + io.protocol - + '/' + this.name + '/' + this.sessid; - }; + if (this.transport) { + debug('clearing existing transport %s', this.transport.name); + this.transport.removeAllListeners(); + } - /** - * Checks if the transport is ready to start a connection. - * - * @param {Socket} socket The socket instance that needs a transport - * @param {Function} fn The callback - * @api private - */ + // set up transport + this.transport = transport; + + // set up transport listeners + transport + .on('drain', function(){ + self.onDrain(); + }) + .on('packet', function(packet){ + self.onPacket(packet); + }) + .on('error', function(e){ + self.onError(e); + }) + .on('close', function(){ + self.onClose('transport close'); + }); +}; - Transport.prototype.ready = function (socket, fn) { - fn.call(this); - }; -})( - 'undefined' != typeof io ? io : module.exports - , 'undefined' != typeof io ? io : module.parent.exports -); -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io, global) { - - /** - * Expose constructor. - */ - - exports.Socket = Socket; - - /** - * Create a new `Socket.IO client` which can establish a persistent - * connection with a Socket.IO enabled server. - * - * @api public - */ - - function Socket (options) { - this.options = { - port: 80 - , secure: false - , document: 'document' in global ? document : false - , resource: 'socket.io' - , transports: io.transports - , 'connect timeout': 10000 - , 'try multiple transports': true - , 'reconnect': true - , 'reconnection delay': 500 - , 'reconnection limit': Infinity - , 'reopen delay': 3000 - , 'max reconnection attempts': 10 - , 'sync disconnect on unload': false - , 'auto connect': true - , 'flash policy port': 10843 - , 'manualFlush': false - }; +/** + * Probes a transport. + * + * @param {String} transport name + * @api private + */ - io.util.merge(this.options, options); +Socket.prototype.probe = function (name) { + debug('probing transport "%s"', name); + var transport = this.createTransport(name, { probe: 1 }) + , failed = false + , self = this; - this.connected = false; - this.open = false; - this.connecting = false; - this.reconnecting = false; - this.namespaces = {}; - this.buffer = []; - this.doBuffer = false; + Socket.priorWebsocketSuccess = false; - if (this.options['sync disconnect on unload'] && - (!this.isXDomain() || io.util.ua.hasCORS)) { - var self = this; - io.util.on(global, 'beforeunload', function () { - self.disconnectSync(); - }, false); + function onTransportOpen(){ + if (self.onlyBinaryUpgrades) { + var upgradeLosesBinary = !this.supportsBinary && self.transport.supportsBinary; + failed = failed || upgradeLosesBinary; } + if (failed) return; + + debug('probe transport "%s" opened', name); + transport.send([{ type: 'ping', data: 'probe' }]); + transport.once('packet', function (msg) { + if (failed) return; + if ('pong' == msg.type && 'probe' == msg.data) { + debug('probe transport "%s" pong', name); + self.upgrading = true; + self.emit('upgrading', transport); + Socket.priorWebsocketSuccess = 'websocket' == transport.name; + + debug('pausing current transport "%s"', self.transport.name); + self.transport.pause(function () { + if (failed) return; + if ('closed' == self.readyState || 'closing' == self.readyState) { + return; + } + debug('changing transport and sending upgrade packet'); - if (this.options['auto connect']) { - this.connect(); - } -}; + cleanup(); - /** - * Apply EventEmitter mixin. - */ + self.setTransport(transport); + transport.send([{ type: 'upgrade' }]); + self.emit('upgrade', transport); + transport = null; + self.upgrading = false; + self.flush(); + }); + } else { + debug('probe transport "%s" failed', name); + var err = new Error('probe error'); + err.transport = transport.name; + self.emit('upgradeError', err); + } + }); + } - io.util.mixin(Socket, io.EventEmitter); + function freezeTransport() { + if (failed) return; - /** - * Returns a namespace listener/emitter for this socket - * - * @api public - */ + // Any callback called by transport should be ignored since now + failed = true; - Socket.prototype.of = function (name) { - if (!this.namespaces[name]) { - this.namespaces[name] = new io.SocketNamespace(this, name); + cleanup(); - if (name !== '') { - this.namespaces[name].packet({ type: 'connect' }); - } - } + transport.close(); + transport = null; + } - return this.namespaces[name]; - }; + //Handle any error that happens while probing + function onerror(err) { + var error = new Error('probe error: ' + err); + error.transport = transport.name; - /** - * Emits the given event to the Socket and all namespaces - * - * @api private - */ + freezeTransport(); - Socket.prototype.publish = function () { - this.emit.apply(this, arguments); + debug('probe transport "%s" failed because of error: %s', name, err); - var nsp; + self.emit('upgradeError', error); + } - for (var i in this.namespaces) { - if (this.namespaces.hasOwnProperty(i)) { - nsp = this.of(i); - nsp.$emit.apply(nsp, arguments); - } - } - }; + function onTransportClose(){ + onerror("transport closed"); + } - /** - * Performs the handshake - * - * @api private - */ + //When the socket is closed while we're probing + function onclose(){ + onerror("socket closed"); + } - function empty () { }; + //When the socket is upgraded while we're probing + function onupgrade(to){ + if (transport && to.name != transport.name) { + debug('"%s" works - aborting "%s"', to.name, transport.name); + freezeTransport(); + } + } - Socket.prototype.handshake = function (fn) { - var self = this - , options = this.options; + //Remove all listeners on the transport and on self + function cleanup(){ + transport.removeListener('open', onTransportOpen); + transport.removeListener('error', onerror); + transport.removeListener('close', onTransportClose); + self.removeListener('close', onclose); + self.removeListener('upgrading', onupgrade); + } - function complete (data) { - if (data instanceof Error) { - self.connecting = false; - self.onError(data.message); - } else { - fn.apply(null, data.split(':')); - } - }; + transport.once('open', onTransportOpen); + transport.once('error', onerror); + transport.once('close', onTransportClose); - var url = [ - 'http' + (options.secure ? 's' : '') + ':/' - , options.host + ':' + options.port - , options.resource - , io.protocol - , io.util.query(this.options.query, 't=' + +new Date) - ].join('/'); + this.once('close', onclose); + this.once('upgrading', onupgrade); - if (this.isXDomain() && !io.util.ua.hasCORS) { - var insertAt = document.getElementsByTagName('script')[0] - , script = document.createElement('script'); + transport.open(); - script.src = url + '&jsonp=' + io.j.length; - insertAt.parentNode.insertBefore(script, insertAt); +}; - io.j.push(function (data) { - complete(data); - script.parentNode.removeChild(script); - }); - } else { - var xhr = io.util.request(); +/** + * Called when connection is deemed open. + * + * @api public + */ - xhr.open('GET', url, true); - if (this.isXDomain()) { - xhr.withCredentials = true; - } - xhr.onreadystatechange = function () { - if (xhr.readyState == 4) { - xhr.onreadystatechange = empty; - - if (xhr.status == 200) { - complete(xhr.responseText); - } else if (xhr.status == 403) { - self.onError(xhr.responseText); - } else { - self.connecting = false; - !self.reconnecting && self.onError(xhr.responseText); - } - } - }; - xhr.send(null); +Socket.prototype.onOpen = function () { + debug('socket open'); + this.readyState = 'open'; + Socket.priorWebsocketSuccess = 'websocket' == this.transport.name; + this.emit('open'); + this.flush(); + + // we check for `readyState` in case an `open` + // listener already closed the socket + if ('open' == this.readyState && this.upgrade && this.transport.pause) { + debug('starting upgrade probes'); + for (var i = 0, l = this.upgrades.length; i < l; i++) { + this.probe(this.upgrades[i]); } - }; - - /** - * Find an available transport based on the options supplied in the constructor. - * - * @api private - */ + } +}; - Socket.prototype.getTransport = function (override) { - var transports = override || this.transports, match; +/** + * Handles a packet. + * + * @api private + */ - for (var i = 0, transport; transport = transports[i]; i++) { - if (io.Transport[transport] - && io.Transport[transport].check(this) - && (!this.isXDomain() || io.Transport[transport].xdomainCheck(this))) { - return new io.Transport[transport](this, this.sessionid); - } - } +Socket.prototype.onPacket = function (packet) { + if ('opening' == this.readyState || 'open' == this.readyState) { + debug('socket receive: type "%s", data "%s"', packet.type, packet.data); - return null; - }; + this.emit('packet', packet); - /** - * Connects to the server. - * - * @param {Function} [fn] Callback. - * @returns {io.Socket} - * @api public - */ + // Socket is live - any packet counts + this.emit('heartbeat'); - Socket.prototype.connect = function (fn) { - if (this.connecting) { - return this; - } + switch (packet.type) { + case 'open': + this.onHandshake(parsejson(packet.data)); + break; - var self = this; - self.connecting = true; - - this.handshake(function (sid, heartbeat, close, transports) { - self.sessionid = sid; - self.closeTimeout = close * 1000; - self.heartbeatTimeout = heartbeat * 1000; - if(!self.transports) - self.transports = self.origTransports = (transports ? io.util.intersect( - transports.split(',') - , self.options.transports - ) : self.options.transports); - - self.setHeartbeatTimeout(); - - function connect (transports){ - if (self.transport) self.transport.clearTimeouts(); - - self.transport = self.getTransport(transports); - if (!self.transport) return self.publish('connect_failed'); - - // once the transport is ready - self.transport.ready(self, function () { - self.connecting = true; - self.publish('connecting', self.transport.name); - self.transport.open(); - - if (self.options['connect timeout']) { - self.connectTimeoutTimer = setTimeout(function () { - if (!self.connected) { - self.connecting = false; - - if (self.options['try multiple transports']) { - var remaining = self.transports; - - while (remaining.length > 0 && remaining.splice(0,1)[0] != - self.transport.name) {} - - if (remaining.length){ - connect(remaining); - } else { - self.publish('connect_failed'); - } - } - } - }, self.options['connect timeout']); - } - }); - } + case 'pong': + this.setPing(); + break; - connect(self.transports); + case 'error': + var err = new Error('server error'); + err.code = packet.data; + this.emit('error', err); + break; - self.once('connect', function (){ - clearTimeout(self.connectTimeoutTimer); + case 'message': + this.emit('data', packet.data); + this.emit('message', packet.data); + break; + } + } else { + debug('packet received with socket readyState "%s"', this.readyState); + } +}; - fn && typeof fn == 'function' && fn(); - }); - }); +/** + * Called upon handshake completion. + * + * @param {Object} handshake obj + * @api private + */ - return this; - }; +Socket.prototype.onHandshake = function (data) { + this.emit('handshake', data); + this.id = data.sid; + this.transport.query.sid = data.sid; + this.upgrades = this.filterUpgrades(data.upgrades); + this.pingInterval = data.pingInterval; + this.pingTimeout = data.pingTimeout; + this.onOpen(); + // In case open handler closes socket + if ('closed' == this.readyState) return; + this.setPing(); + + // Prolong liveness of socket on heartbeat + this.removeListener('heartbeat', this.onHeartbeat); + this.on('heartbeat', this.onHeartbeat); +}; - /** - * Clears and sets a new heartbeat timeout using the value given by the - * server during the handshake. - * - * @api private - */ +/** + * Resets ping timeout. + * + * @api private + */ - Socket.prototype.setHeartbeatTimeout = function () { - clearTimeout(this.heartbeatTimeoutTimer); - if(this.transport && !this.transport.heartbeats()) return; +Socket.prototype.onHeartbeat = function (timeout) { + clearTimeout(this.pingTimeoutTimer); + var self = this; + self.pingTimeoutTimer = setTimeout(function () { + if ('closed' == self.readyState) return; + self.onClose('ping timeout'); + }, timeout || (self.pingInterval + self.pingTimeout)); +}; - var self = this; - this.heartbeatTimeoutTimer = setTimeout(function () { - self.transport.onClose(); - }, this.heartbeatTimeout); - }; +/** + * Pings server every `this.pingInterval` and expects response + * within `this.pingTimeout` or closes connection. + * + * @api private + */ - /** - * Sends a message. - * - * @param {Object} data packet. - * @returns {io.Socket} - * @api public - */ - - Socket.prototype.packet = function (data) { - if (this.connected && !this.doBuffer) { - this.transport.packet(data); - } else { - this.buffer.push(data); - } +Socket.prototype.setPing = function () { + var self = this; + clearTimeout(self.pingIntervalTimer); + self.pingIntervalTimer = setTimeout(function () { + debug('writing ping packet - expecting pong within %sms', self.pingTimeout); + self.ping(); + self.onHeartbeat(self.pingTimeout); + }, self.pingInterval); +}; - return this; - }; +/** +* Sends a ping packet. +* +* @api public +*/ - /** - * Sets buffer state - * - * @api private - */ +Socket.prototype.ping = function () { + this.sendPacket('ping'); +}; - Socket.prototype.setBuffer = function (v) { - this.doBuffer = v; +/** + * Called on `drain` event + * + * @api private + */ - if (!v && this.connected && this.buffer.length) { - if (!this.options['manualFlush']) { - this.flushBuffer(); - } +Socket.prototype.onDrain = function() { + for (var i = 0; i < this.prevBufferLen; i++) { + if (this.callbackBuffer[i]) { + this.callbackBuffer[i](); } - }; + } - /** - * Flushes the buffer data over the wire. - * To be invoked manually when 'manualFlush' is set to true. - * - * @api public - */ + this.writeBuffer.splice(0, this.prevBufferLen); + this.callbackBuffer.splice(0, this.prevBufferLen); - Socket.prototype.flushBuffer = function() { - this.transport.payload(this.buffer); - this.buffer = []; - }; - + // setting prevBufferLen = 0 is very important + // for example, when upgrading, upgrade packet is sent over, + // and a nonzero prevBufferLen could cause problems on `drain` + this.prevBufferLen = 0; - /** - * Disconnect the established connect. - * - * @returns {io.Socket} - * @api public - */ - - Socket.prototype.disconnect = function () { - if (this.connected || this.connecting) { - if (this.open) { - this.of('').packet({ type: 'disconnect' }); - } + if (this.writeBuffer.length == 0) { + this.emit('drain'); + } else { + this.flush(); + } +}; - // handle disconnection immediately - this.onDisconnect('booted'); - } +/** + * Flush write buffers. + * + * @api private + */ - return this; - }; +Socket.prototype.flush = function () { + if ('closed' != this.readyState && this.transport.writable && + !this.upgrading && this.writeBuffer.length) { + debug('flushing %d packets in socket', this.writeBuffer.length); + this.transport.send(this.writeBuffer); + // keep track of current length of writeBuffer + // splice writeBuffer and callbackBuffer on `drain` + this.prevBufferLen = this.writeBuffer.length; + this.emit('flush'); + } +}; - /** - * Disconnects the socket with a sync XHR. - * - * @api private - */ - - Socket.prototype.disconnectSync = function () { - // ensure disconnection - var xhr = io.util.request(); - var uri = [ - 'http' + (this.options.secure ? 's' : '') + ':/' - , this.options.host + ':' + this.options.port - , this.options.resource - , io.protocol - , '' - , this.sessionid - ].join('/') + '/?disconnect=1'; - - xhr.open('GET', uri, false); - xhr.send(null); - - // handle disconnection immediately - this.onDisconnect('booted'); - }; +/** + * Sends a message. + * + * @param {String} message. + * @param {Function} callback function. + * @return {Socket} for chaining. + * @api public + */ - /** - * Check if we need to use cross domain enabled transports. Cross domain would - * be a different port or different domain name. - * - * @returns {Boolean} - * @api private - */ +Socket.prototype.write = +Socket.prototype.send = function (msg, fn) { + this.sendPacket('message', msg, fn); + return this; +}; - Socket.prototype.isXDomain = function () { +/** + * Sends a packet. + * + * @param {String} packet type. + * @param {String} data. + * @param {Function} callback function. + * @api private + */ - var port = global.location.port || - ('https:' == global.location.protocol ? 443 : 80); +Socket.prototype.sendPacket = function (type, data, fn) { + var packet = { type: type, data: data }; + this.emit('packetCreate', packet); + this.writeBuffer.push(packet); + this.callbackBuffer.push(fn); + this.flush(); +}; - return this.options.host !== global.location.hostname - || this.options.port != port; - }; +/** + * Closes the connection. + * + * @api private + */ - /** - * Called upon handshake. - * - * @api private - */ - - Socket.prototype.onConnect = function () { - if (!this.connected) { - this.connected = true; - this.connecting = false; - if (!this.doBuffer) { - // make sure to flush the buffer - this.setBuffer(false); - } - this.emit('connect'); - } - }; +Socket.prototype.close = function () { + if ('opening' == this.readyState || 'open' == this.readyState) { + this.onClose('forced close'); + debug('socket closing - telling transport to close'); + this.transport.close(); + } - /** - * Called when the transport opens - * - * @api private - */ + return this; +}; - Socket.prototype.onOpen = function () { - this.open = true; - }; +/** + * Called upon transport error + * + * @api private + */ - /** - * Called when the transport closes. - * - * @api private - */ +Socket.prototype.onError = function (err) { + debug('socket error %j', err); + Socket.priorWebsocketSuccess = false; + this.emit('error', err); + this.onClose('transport error', err); +}; - Socket.prototype.onClose = function () { - this.open = false; - clearTimeout(this.heartbeatTimeoutTimer); - }; +/** + * Called upon transport close. + * + * @api private + */ - /** - * Called when the transport first opens a connection - * - * @param text - */ +Socket.prototype.onClose = function (reason, desc) { + if ('opening' == this.readyState || 'open' == this.readyState) { + debug('socket close with reason: "%s"', reason); + var self = this; - Socket.prototype.onPacket = function (packet) { - this.of(packet.endpoint).onPacket(packet); - }; + // clear timers + clearTimeout(this.pingIntervalTimer); + clearTimeout(this.pingTimeoutTimer); - /** - * Handles an error. - * - * @api private - */ - - Socket.prototype.onError = function (err) { - if (err && err.advice) { - if (err.advice === 'reconnect' && (this.connected || this.connecting)) { - this.disconnect(); - if (this.options.reconnect) { - this.reconnect(); - } - } - } + // clean buffers in next tick, so developers can still + // grab the buffers on `close` event + setTimeout(function() { + self.writeBuffer = []; + self.callbackBuffer = []; + self.prevBufferLen = 0; + }, 0); - this.publish('error', err && err.reason ? err.reason : err); - }; + // stop event from firing again for transport + this.transport.removeAllListeners('close'); - /** - * Called when the transport disconnects. - * - * @api private - */ + // ensure transport won't stay open + this.transport.close(); - Socket.prototype.onDisconnect = function (reason) { - var wasConnected = this.connected - , wasConnecting = this.connecting; + // ignore further transport communication + this.transport.removeAllListeners(); - this.connected = false; - this.connecting = false; - this.open = false; + // set ready state + this.readyState = 'closed'; - if (wasConnected || wasConnecting) { - this.transport.close(); - this.transport.clearTimeouts(); - if (wasConnected) { - this.publish('disconnect', reason); + // clear session id + this.id = null; - if ('booted' != reason && this.options.reconnect && !this.reconnecting) { - this.reconnect(); - } - } - } - }; + // emit close event + this.emit('close', reason, desc); + } +}; - /** - * Called upon reconnection. - * - * @api private - */ +/** + * Filters upgrades, returning only those matching client transports. + * + * @param {Array} server upgrades + * @api private + * + */ - Socket.prototype.reconnect = function () { - this.reconnecting = true; - this.reconnectionAttempts = 0; - this.reconnectionDelay = this.options['reconnection delay']; - - var self = this - , maxAttempts = this.options['max reconnection attempts'] - , tryMultiple = this.options['try multiple transports'] - , limit = this.options['reconnection limit']; - - function reset () { - if (self.connected) { - for (var i in self.namespaces) { - if (self.namespaces.hasOwnProperty(i) && '' !== i) { - self.namespaces[i].packet({ type: 'connect' }); - } - } - self.publish('reconnect', self.transport.name, self.reconnectionAttempts); - } +Socket.prototype.filterUpgrades = function (upgrades) { + var filteredUpgrades = []; + for (var i = 0, j = upgrades.length; i= maxAttempts) { - if (!self.redoTransports) { - self.on('connect_failed', maybeReconnect); - self.options['try multiple transports'] = true; - self.transports = self.origTransports; - self.transport = self.getTransport(); - self.redoTransports = true; - self.connect(); - } else { - self.publish('reconnect_failed'); - reset(); - } - } else { - if (self.reconnectionDelay < limit) { - self.reconnectionDelay *= 2; // exponential back off - } +/** + * A counter used to prevent collisions in the timestamps used + * for cache busting. + */ - self.connect(); - self.publish('reconnecting', self.reconnectionDelay, self.reconnectionAttempts); - self.reconnectionTimer = setTimeout(maybeReconnect, self.reconnectionDelay); - } - }; +Transport.timestamps = 0; - this.options['try multiple transports'] = false; - this.reconnectionTimer = setTimeout(maybeReconnect, this.reconnectionDelay); +/** + * Emits an error. + * + * @param {String} str + * @return {Transport} for chaining + * @api public + */ - this.on('connect', maybeReconnect); - }; +Transport.prototype.onError = function (msg, desc) { + var err = new Error(msg); + err.type = 'TransportError'; + err.description = desc; + this.emit('error', err); + return this; +}; -})( - 'undefined' != typeof io ? io : module.exports - , 'undefined' != typeof io ? io : module.parent.exports - , this -); /** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed + * Opens the transport. + * + * @api public */ -(function (exports, io) { +Transport.prototype.open = function () { + if ('closed' == this.readyState || '' == this.readyState) { + this.readyState = 'opening'; + this.doOpen(); + } - /** - * Expose constructor. - */ + return this; +}; - exports.SocketNamespace = SocketNamespace; +/** + * Closes the transport. + * + * @api private + */ - /** - * Socket namespace constructor. - * - * @constructor - * @api public - */ +Transport.prototype.close = function () { + if ('opening' == this.readyState || 'open' == this.readyState) { + this.doClose(); + this.onClose(); + } - function SocketNamespace (socket, name) { - this.socket = socket; - this.name = name || ''; - this.flags = {}; - this.json = new Flag(this, 'json'); - this.ackPackets = 0; - this.acks = {}; - }; + return this; +}; - /** - * Apply EventEmitter mixin. - */ +/** + * Sends multiple packets. + * + * @param {Array} packets + * @api private + */ - io.util.mixin(SocketNamespace, io.EventEmitter); +Transport.prototype.send = function(packets){ + if ('open' == this.readyState) { + this.write(packets); + } else { + throw new Error('Transport not open'); + } +}; - /** - * Copies emit since we override it - * - * @api private - */ +/** + * Called upon open + * + * @api private + */ - SocketNamespace.prototype.$emit = io.EventEmitter.prototype.emit; +Transport.prototype.onOpen = function () { + this.readyState = 'open'; + this.writable = true; + this.emit('open'); +}; - /** - * Creates a new namespace, by proxying the request to the socket. This - * allows us to use the synax as we do on the server. - * - * @api public - */ +/** + * Called with data. + * + * @param {String} data + * @api private + */ - SocketNamespace.prototype.of = function () { - return this.socket.of.apply(this.socket, arguments); - }; +Transport.prototype.onData = function(data){ + try { + var packet = parser.decodePacket(data, this.socket.binaryType); + this.onPacket(packet); + } catch(e){ + e.data = data; + this.onError('parser decode error', e); + } +}; - /** - * Sends a packet. - * - * @api private - */ +/** + * Called with a decoded packet. + */ - SocketNamespace.prototype.packet = function (packet) { - packet.endpoint = this.name; - this.socket.packet(packet); - this.flags = {}; - return this; - }; +Transport.prototype.onPacket = function (packet) { + this.emit('packet', packet); +}; - /** - * Sends a message - * - * @api public - */ +/** + * Called upon close. + * + * @api private + */ - SocketNamespace.prototype.send = function (data, fn) { - var packet = { - type: this.flags.json ? 'json' : 'message' - , data: data - }; +Transport.prototype.onClose = function () { + this.readyState = 'closed'; + this.emit('close'); +}; - if ('function' == typeof fn) { - packet.id = ++this.ackPackets; - packet.ack = true; - this.acks[packet.id] = fn; - } +},{"component-emitter":8,"engine.io-parser":22}],15:[function(require,module,exports){ +var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};/** + * Module dependencies + */ - return this.packet(packet); - }; +var XMLHttpRequest = require('xmlhttprequest'); +var XHR = require('./polling-xhr'); +var JSONP = require('./polling-jsonp'); +var websocket = require('./websocket'); - /** - * Emits an event - * - * @api public - */ - - SocketNamespace.prototype.emit = function (name) { - var args = Array.prototype.slice.call(arguments, 1) - , lastArg = args[args.length - 1] - , packet = { - type: 'event' - , name: name - }; +/** + * Export transports. + */ - if ('function' == typeof lastArg) { - packet.id = ++this.ackPackets; - packet.ack = 'data'; - this.acks[packet.id] = lastArg; - args = args.slice(0, args.length - 1); - } +exports.polling = polling; +exports.websocket = websocket; - packet.args = args; +/** + * Polling transport polymorphic constructor. + * Decides on xhr vs jsonp based on feature detection. + * + * @api private + */ - return this.packet(packet); - }; +function polling(opts){ + var xhr; + var xd = false; - /** - * Disconnects the namespace - * - * @api private - */ + if (global.location) { + var isSSL = 'https:' == location.protocol; + var port = location.port; - SocketNamespace.prototype.disconnect = function () { - if (this.name === '') { - this.socket.disconnect(); - } else { - this.packet({ type: 'disconnect' }); - this.$emit('disconnect'); + // some user agents have empty `location.port` + if (!port) { + port = isSSL ? 443 : 80; } - return this; - }; + xd = opts.hostname != location.hostname || port != opts.port; + } - /** - * Handles a packet - * - * @api private - */ + opts.xdomain = xd; + xhr = new XMLHttpRequest(opts); - SocketNamespace.prototype.onPacket = function (packet) { - var self = this; + if ('open' in xhr && !opts.forceJSONP) { + return new XHR(opts); + } else { + return new JSONP(opts); + } +} - function ack () { - self.packet({ - type: 'ack' - , args: io.util.toArray(arguments) - , ackId: packet.id - }); - }; +},{"./polling-jsonp":16,"./polling-xhr":17,"./websocket":19,"xmlhttprequest":20}],16:[function(require,module,exports){ +var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}; +/** + * Module requirements. + */ - switch (packet.type) { - case 'connect': - this.$emit('connect'); - break; +var Polling = require('./polling'); +var inherit = require('component-inherit'); - case 'disconnect': - if (this.name === '') { - this.socket.onDisconnect(packet.reason || 'booted'); - } else { - this.$emit('disconnect', packet.reason); - } - break; +/** + * Module exports. + */ - case 'message': - case 'json': - var params = ['message', packet.data]; +module.exports = JSONPPolling; - if (packet.ack == 'data') { - params.push(ack); - } else if (packet.ack) { - this.packet({ type: 'ack', ackId: packet.id }); - } +/** + * Cached regular expressions. + */ - this.$emit.apply(this, params); - break; +var rNewline = /\n/g; +var rEscapedNewline = /\\n/g; - case 'event': - var params = [packet.name].concat(packet.args); +/** + * Global JSONP callbacks. + */ - if (packet.ack == 'data') - params.push(ack); +var callbacks; - this.$emit.apply(this, params); - break; +/** + * Callbacks count. + */ - case 'ack': - if (this.acks[packet.ackId]) { - this.acks[packet.ackId].apply(this, packet.args); - delete this.acks[packet.ackId]; - } - break; +var index = 0; - case 'error': - if (packet.advice){ - this.socket.onError(packet); - } else { - if (packet.reason == 'unauthorized') { - this.$emit('connect_failed', packet.reason); - } else { - this.$emit('error', packet.reason); - } - } - break; - } - }; +/** + * Noop. + */ - /** - * Flag interface. - * - * @api private - */ +function empty () { } - function Flag (nsp, name) { - this.namespace = nsp; - this.name = name; - }; +/** + * JSONP Polling constructor. + * + * @param {Object} opts. + * @api public + */ - /** - * Send a message - * - * @api public - */ +function JSONPPolling (opts) { + Polling.call(this, opts); - Flag.prototype.send = function () { - this.namespace.flags[this.name] = true; - this.namespace.send.apply(this.namespace, arguments); - }; + this.query = this.query || {}; + + // define global callbacks array if not present + // we do this here (lazily) to avoid unneeded global pollution + if (!callbacks) { + // we need to consider multiple engines in the same page + if (!global.___eio) global.___eio = []; + callbacks = global.___eio; + } - /** - * Emit an event - * - * @api public - */ + // callback identifier + this.index = callbacks.length; - Flag.prototype.emit = function () { - this.namespace.flags[this.name] = true; - this.namespace.emit.apply(this.namespace, arguments); - }; + // add callback to jsonp global + var self = this; + callbacks.push(function (msg) { + self.onData(msg); + }); -})( - 'undefined' != typeof io ? io : module.exports - , 'undefined' != typeof io ? io : module.parent.exports -); + // append to query string + this.query.j = this.index; + + // prevent spurious errors from being emitted when the window is unloaded + if (global.document && global.addEventListener) { + global.addEventListener('beforeunload', function () { + if (self.script) self.script.onerror = empty; + }); + } +} /** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed + * Inherits from Polling. */ -(function (exports, io, global) { +inherit(JSONPPolling, Polling); - /** - * Expose constructor. - */ +/* + * JSONP only supports binary as base64 encoded strings + */ - exports.websocket = WS; +JSONPPolling.prototype.supportsBinary = false; - /** - * The WebSocket transport uses the HTML5 WebSocket API to establish an - * persistent connection with the Socket.IO server. This transport will also - * be inherited by the FlashSocket fallback as it provides a API compatible - * polyfill for the WebSockets. - * - * @constructor - * @extends {io.Transport} - * @api public - */ +/** + * Closes the socket. + * + * @api private + */ - function WS (socket) { - io.Transport.apply(this, arguments); - }; +JSONPPolling.prototype.doClose = function () { + if (this.script) { + this.script.parentNode.removeChild(this.script); + this.script = null; + } - /** - * Inherits from Transport. - */ + if (this.form) { + this.form.parentNode.removeChild(this.form); + this.form = null; + } - io.util.inherit(WS, io.Transport); + Polling.prototype.doClose.call(this); +}; - /** - * Transport name - * - * @api public - */ +/** + * Starts a poll cycle. + * + * @api private + */ - WS.prototype.name = 'websocket'; +JSONPPolling.prototype.doPoll = function () { + var self = this; + var script = document.createElement('script'); - /** - * Initializes a new `WebSocket` connection with the Socket.IO server. We attach - * all the appropriate listeners to handle the responses from the server. - * - * @returns {Transport} - * @api public - */ + if (this.script) { + this.script.parentNode.removeChild(this.script); + this.script = null; + } - WS.prototype.open = function () { - var query = io.util.query(this.socket.options.query) - , self = this - , Socket + script.async = true; + script.src = this.uri(); + script.onerror = function(e){ + self.onError('jsonp poll error',e); + }; + var insertAt = document.getElementsByTagName('script')[0]; + insertAt.parentNode.insertBefore(script, insertAt); + this.script = script; - if (!Socket) { - Socket = global.MozWebSocket || global.WebSocket; - } + var isUAgecko = 'undefined' != typeof navigator && /gecko/i.test(navigator.userAgent); + + if (isUAgecko) { + setTimeout(function () { + var iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + document.body.removeChild(iframe); + }, 100); + } +}; - this.websocket = new Socket(this.prepareUrl() + query); +/** + * Writes with a hidden iframe. + * + * @param {String} data to send + * @param {Function} called upon flush. + * @api private + */ - this.websocket.onopen = function () { - self.onOpen(); - self.socket.setBuffer(false); - }; - this.websocket.onmessage = function (ev) { - self.onData(ev.data); - }; - this.websocket.onclose = function () { - self.onClose(); - self.socket.setBuffer(true); - }; - this.websocket.onerror = function (e) { - self.onError(e); - }; +JSONPPolling.prototype.doWrite = function (data, fn) { + var self = this; + + if (!this.form) { + var form = document.createElement('form'); + var area = document.createElement('textarea'); + var id = this.iframeId = 'eio_iframe_' + this.index; + var iframe; + + form.className = 'socketio'; + form.style.position = 'absolute'; + form.style.top = '-1000px'; + form.style.left = '-1000px'; + form.target = id; + form.method = 'POST'; + form.setAttribute('accept-charset', 'utf-8'); + area.name = 'd'; + form.appendChild(area); + document.body.appendChild(form); + + this.form = form; + this.area = area; + } - return this; - }; + this.form.action = this.uri(); - /** - * Send a message to the Socket.IO server. The message will automatically be - * encoded in the correct message format. - * - * @returns {Transport} - * @api public - */ - - // Do to a bug in the current IDevices browser, we need to wrap the send in a - // setTimeout, when they resume from sleeping the browser will crash if - // we don't allow the browser time to detect the socket has been closed - if (io.util.ua.iDevice) { - WS.prototype.send = function (data) { - var self = this; - setTimeout(function() { - self.websocket.send(data); - },0); - return this; - }; - } else { - WS.prototype.send = function (data) { - this.websocket.send(data); - return this; - }; + function complete () { + initIframe(); + fn(); } - /** - * Payload - * - * @api private - */ - - WS.prototype.payload = function (arr) { - for (var i = 0, l = arr.length; i < l; i++) { - this.packet(arr[i]); + function initIframe () { + if (self.iframe) { + try { + self.form.removeChild(self.iframe); + } catch (e) { + self.onError('jsonp polling iframe removal error', e); + } } - return this; - }; - - /** - * Disconnect the established `WebSocket` connection. - * - * @returns {Transport} - * @api public - */ - - WS.prototype.close = function () { - this.websocket.close(); - return this; - }; - /** - * Handle the errors that `WebSocket` might be giving when we - * are attempting to connect or send messages. - * - * @param {Error} e The error. - * @api private - */ + try { + // ie6 dynamic iframes with target="" support (thanks Chris Lambacher) + var html = '