python-engineio-1.6.1/0000755_j0000000000013124366231015156 5ustar migu778100000000000000python-engineio-1.6.1/engineio/0000755_j0000000000013124366231016753 5ustar migu778100000000000000python-engineio-1.6.1/engineio/__init__.py0000644_j0000000056613124364616021100 0ustar migu778100000000000000import sys from .middleware import Middleware from .server import Server if sys.version_info >= (3, 5): # pragma: no cover from .asyncio_server import AsyncServer else: # pragma: no cover AsyncServer = None __version__ = '1.6.1' __all__ = ['__version__', 'Middleware', 'Server'] if AsyncServer is not None: # pragma: no cover __all__.append('AsyncServer') python-engineio-1.6.1/engineio/async_aiohttp.py0000644_j0000000700413123231147022167 0ustar migu778100000000000000import sys from urllib.parse import urlsplit import aiohttp import six def create_route(app, engineio_server, engineio_endpoint): """This function sets up the engine.io endpoint as a route for the application. Note that both GET and POST requests must be hooked up on the engine.io endpoint. """ app.router.add_get(engineio_endpoint, engineio_server.handle_request) app.router.add_post(engineio_endpoint, engineio_server.handle_request) def translate_request(request): """This function takes the arguments passed to the request handler and uses them to generate a WSGI compatible environ dictionary. """ message = request._message payload = request._payload uri_parts = urlsplit(message.path) environ = { 'wsgi.input': payload, 'wsgi.errors': sys.stderr, 'wsgi.version': (1, 0), 'wsgi.async': True, 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'SERVER_SOFTWARE': 'aiohttp', 'REQUEST_METHOD': message.method, 'QUERY_STRING': uri_parts.query or '', 'RAW_URI': message.path, 'SERVER_PROTOCOL': 'HTTP/%s.%s' % message.version, 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': '0', 'SERVER_NAME': 'aiohttp', 'SERVER_PORT': '0', 'aiohttp.request': request } for hdr_name, hdr_value in message.headers.items(): hdr_name = hdr_name.upper() if hdr_name == 'CONTENT-TYPE': environ['CONTENT_TYPE'] = hdr_value continue elif hdr_name == 'CONTENT-LENGTH': environ['CONTENT_LENGTH'] = hdr_value continue key = 'HTTP_%s' % hdr_name.replace('-', '_') if key in environ: hdr_value = '%s,%s' % (environ[key], hdr_value) environ[key] = hdr_value environ['wsgi.url_scheme'] = environ.get('HTTP_X_FORWARDED_PROTO', 'http') path_info = uri_parts.path environ['PATH_INFO'] = path_info environ['SCRIPT_NAME'] = '' return environ def make_response(status, headers, payload): """This function generates an appropriate response object for this async mode. """ return aiohttp.web.Response(body=payload, status=int(status.split()[0]), headers=headers) class WebSocket(object): # pragma: no cover """ This wrapper class provides a aiohttp WebSocket interface that is somewhat compatible with eventlet's implementation. """ def __init__(self, handler): self.handler = handler self._sock = None async def __call__(self, environ): request = environ['aiohttp.request'] self._sock = aiohttp.web.WebSocketResponse() await self._sock.prepare(request) self.environ = environ await self.handler(self) return self._sock async def close(self): await self._sock.close() async def send(self, message): if isinstance(message, bytes): self._sock.send_bytes(message) else: self._sock.send_str(message) async def wait(self): msg = await self._sock.receive() if not isinstance(msg.data, six.binary_type) and \ not isinstance(msg.data, six.text_type): raise IOError() return msg.data _async = { 'asyncio': True, 'create_route': create_route, 'translate_request': translate_request, 'make_response': make_response, 'websocket': sys.modules[__name__], 'websocket_class': 'WebSocket' } python-engineio-1.6.1/engineio/async_eventlet.py0000644_j0000000177713103402643022357 0ustar migu778100000000000000import importlib import sys from eventlet import sleep from eventlet.websocket import WebSocketWSGI as _WebSocketWSGI class WebSocketWSGI(_WebSocketWSGI): def __init__(self, *args, **kwargs): super(WebSocketWSGI, self).__init__(*args, **kwargs) self._sock = None def __call__(self, environ, start_response): if 'eventlet.input' not in environ: raise RuntimeError('You need to use the eventlet server. ' 'See the Deployment section of the ' 'documentation for more information.') self._sock = environ['eventlet.input'].get_socket() return super(WebSocketWSGI, self).__call__(environ, start_response) _async = { 'threading': importlib.import_module('eventlet.green.threading'), 'thread_class': 'Thread', 'queue': importlib.import_module('eventlet.queue'), 'queue_class': 'Queue', 'websocket': sys.modules[__name__], 'websocket_class': 'WebSocketWSGI', 'sleep': sleep } python-engineio-1.6.1/engineio/async_gevent.py0000644_j0000000351513057716216022025 0ustar migu778100000000000000import importlib import sys import gevent try: import geventwebsocket # noqa _websocket_available = True except ImportError: _websocket_available = False class Thread(gevent.Greenlet): # pragma: no cover """ This wrapper class provides gevent Greenlet interface that is compatible with the standard library's Thread class. """ def __init__(self, target, args=[], kwargs={}): super(Thread, self).__init__(target, *args, **kwargs) def _run(self): return self.run() class WebSocketWSGI(object): # pragma: no cover """ This wrapper class provides a gevent WebSocket interface that is compatible with eventlet's implementation. """ def __init__(self, app): self.app = app def __call__(self, environ, start_response): if 'wsgi.websocket' not in environ: raise RuntimeError('You need to use the gevent-websocket server. ' 'See the Deployment section of the ' 'documentation for more information.') self._sock = environ['wsgi.websocket'] self.environ = environ self.version = self._sock.version self.path = self._sock.path self.origin = self._sock.origin self.protocol = self._sock.protocol return self.app(self) def close(self): return self._sock.close() def send(self, message): return self._sock.send(message) def wait(self): return self._sock.receive() _async = { 'threading': sys.modules[__name__], 'thread_class': 'Thread', 'queue': importlib.import_module('gevent.queue'), 'queue_class': 'JoinableQueue', 'websocket': sys.modules[__name__] if _websocket_available else None, 'websocket_class': 'WebSocketWSGI' if _websocket_available else None, 'sleep': gevent.sleep } python-engineio-1.6.1/engineio/async_gevent_uwsgi.py0000644_j0000001253513103257704023237 0ustar migu778100000000000000import importlib import sys import six import gevent import uwsgi _websocket_available = hasattr(uwsgi, 'websocket_handshake') class Thread(gevent.Greenlet): # pragma: no cover """ This wrapper class provides gevent Greenlet interface that is compatible with the standard library's Thread class. """ def __init__(self, target, args=[], kwargs={}): super(Thread, self).__init__(target, *args, **kwargs) def _run(self): return self.run() class uWSGIWebSocket(object): # pragma: no cover """ This wrapper class provides a uWSGI WebSocket interface that is compatible with eventlet's implementation. """ def __init__(self, app): self.app = app self._sock = None def __call__(self, environ, start_response): self._sock = uwsgi.connection_fd() self.environ = environ uwsgi.websocket_handshake() self._req_ctx = None if hasattr(uwsgi, 'request_context'): # uWSGI >= 2.1.x with support for api access across-greenlets self._req_ctx = uwsgi.request_context() else: # use event and queue for sending messages from gevent.event import Event from gevent.queue import Queue from gevent.select import select self._event = Event() self._send_queue = Queue() # spawn a select greenlet def select_greenlet_runner(fd, event): """Sets event when data becomes available to read on fd.""" while True: event.set() try: select([fd], [], [])[0] except ValueError: break self._select_greenlet = gevent.spawn( select_greenlet_runner, self._sock, self._event) self.app(self) def close(self): """Disconnects uWSGI from the client.""" uwsgi.disconnect() if self._req_ctx is None: # better kill it here in case wait() is not called again self._select_greenlet.kill() self._event.set() def _send(self, msg): """Transmits message either in binary or UTF-8 text mode, depending on its type.""" if isinstance(msg, six.binary_type): method = uwsgi.websocket_send_binary else: method = uwsgi.websocket_send if self._req_ctx is not None: method(msg, request_context=self._req_ctx) else: method(msg) def _decode_received(self, msg): """Returns either bytes or str, depending on message type.""" if not isinstance(msg, six.binary_type): # already decoded - do nothing return msg # only decode from utf-8 if message is not binary data type = six.byte2int(msg[0:1]) if type >= 48: # no binary return msg.decode('utf-8') # binary message, don't try to decode return msg def send(self, msg): """Queues a message for sending. Real transmission is done in wait method. Sends directly if uWSGI version is new enough.""" if self._req_ctx is not None: self._send(msg) else: self._send_queue.put(msg) self._event.set() def wait(self): """Waits and returns received messages. If running in compatibility mode for older uWSGI versions, it also sends messages that have been queued by send(). A return value of None means that connection was closed. This must be called repeatedly. For uWSGI < 2.1.x it must be called from the main greenlet.""" while True: if self._req_ctx is not None: try: msg = uwsgi.websocket_recv(request_context=self._req_ctx) except IOError: # connection closed return None return self._decode_received(msg) else: # we wake up at least every 3 seconds to let uWSGI # do its ping/ponging event_set = self._event.wait(timeout=3) if event_set: self._event.clear() # maybe there is something to send msgs = [] while True: try: msgs.append(self._send_queue.get(block=False)) except gevent.queue.Empty: break for msg in msgs: self._send(msg) # maybe there is something to receive, if not, at least # ensure uWSGI does its ping/ponging try: msg = uwsgi.websocket_recv_nb() except IOError: # connection closed self._select_greenlet.kill() return None if msg: # message available return self._decode_received(msg) _async = { 'threading': sys.modules[__name__], 'thread_class': 'Thread', 'queue': importlib.import_module('gevent.queue'), 'queue_class': 'JoinableQueue', 'websocket': sys.modules[__name__] if _websocket_available else None, 'websocket_class': 'uWSGIWebSocket' if _websocket_available else None, 'sleep': gevent.sleep } python-engineio-1.6.1/engineio/async_sanic.py0000644_j0000001044213103272717021622 0ustar migu778100000000000000import sys from urllib.parse import urlsplit from sanic.response import HTTPResponse try: from sanic.websocket import WebSocketProtocol except ImportError: # the installed version of sanic does not have websocket support WebSocketProtocol = None import six def create_route(app, engineio_server, engineio_endpoint): """This function sets up the engine.io endpoint as a route for the application. Note that both GET and POST requests must be hooked up on the engine.io endpoint. """ app.add_route(engineio_server.handle_request, engineio_endpoint, methods=['GET', 'POST']) try: app.enable_websocket() except AttributeError: # ignore, this version does not support websocket pass def translate_request(request): """This function takes the arguments passed to the request handler and uses them to generate a WSGI compatible environ dictionary. """ class AwaitablePayload(object): def __init__(self, payload): self.payload = payload or b'' async def read(self, length=None): if length is None: r = self.payload self.payload = b'' else: r = self.payload[:length] self.payload = self.payload[length:] return r uri_parts = urlsplit(request.url) environ = { 'wsgi.input': AwaitablePayload(request.body), 'wsgi.errors': sys.stderr, 'wsgi.version': (1, 0), 'wsgi.async': True, 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'SERVER_SOFTWARE': 'sanic', 'REQUEST_METHOD': request.method, 'QUERY_STRING': uri_parts.query or '', 'RAW_URI': request.url, 'SERVER_PROTOCOL': 'HTTP/' + request.version, 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': '0', 'SERVER_NAME': 'sanic', 'SERVER_PORT': '0', 'sanic.request': request } for hdr_name, hdr_value in request.headers.items(): hdr_name = hdr_name.upper() if hdr_name == 'CONTENT-TYPE': environ['CONTENT_TYPE'] = hdr_value continue elif hdr_name == 'CONTENT-LENGTH': environ['CONTENT_LENGTH'] = hdr_value continue key = 'HTTP_%s' % hdr_name.replace('-', '_') if key in environ: hdr_value = '%s,%s' % (environ[key], hdr_value) environ[key] = hdr_value environ['wsgi.url_scheme'] = environ.get('HTTP_X_FORWARDED_PROTO', 'http') path_info = uri_parts.path environ['PATH_INFO'] = path_info environ['SCRIPT_NAME'] = '' return environ def make_response(status, headers, payload): """This function generates an appropriate response object for this async mode. """ headers_dict = {} content_type = None for h in headers: if h[0].lower() == 'content-type': content_type = h[1] else: headers_dict[h[0]] = h[1] return HTTPResponse(body_bytes=payload, content_type=content_type, status=int(status.split()[0]), headers=headers_dict) class WebSocket(object): # pragma: no cover """ This wrapper class provides a sanic WebSocket interface that is somewhat compatible with eventlet's implementation. """ def __init__(self, handler): self.handler = handler self._sock = None async def __call__(self, environ): request = environ['sanic.request'] protocol = request.transport.get_protocol() self._sock = await protocol.websocket_handshake(request) self.environ = environ await self.handler(self) async def close(self): await self._sock.close() async def send(self, message): await self._sock.send(message) async def wait(self): data = await self._sock.recv() if not isinstance(data, six.binary_type) and \ not isinstance(data, six.text_type): raise IOError() return data _async = { 'asyncio': True, 'create_route': create_route, 'translate_request': translate_request, 'make_response': make_response, 'websocket': sys.modules[__name__] if WebSocketProtocol else None, 'websocket_class': 'WebSocket' if WebSocketProtocol else None } python-engineio-1.6.1/engineio/async_threading.py0000644_j0000000063013057716206022474 0ustar migu778100000000000000import importlib import time try: queue = importlib.import_module('queue') except ImportError: # pragma: no cover queue = importlib.import_module('Queue') # pragma: no cover _async = { 'threading': importlib.import_module('threading'), 'thread_class': 'Thread', 'queue': queue, 'queue_class': 'Queue', 'websocket': None, 'websocket_class': None, 'sleep': time.sleep } python-engineio-1.6.1/engineio/asyncio_server.py0000644_j0000002736713124366223022400 0ustar migu778100000000000000import asyncio import six from six.moves import urllib from .exceptions import EngineIOError from . import packet from . import server from . import asyncio_socket class AsyncServer(server.Server): """An Engine.IO server for asyncio. This class implements a fully compliant Engine.IO web server with support for websocket and long-polling transports, compatible with the asyncio framework on Python 3.5 or newer. :param async_mode: The asynchronous model to use. See the Deployment section in the documentation for a description of the available options. Valid async modes are "aiohttp". If this argument is not given, an async mode is chosen based on the installed packages. :param ping_timeout: The time in seconds that the client waits for the server to respond before disconnecting. :param ping_interval: The interval in seconds at which the client pings the server. :param max_http_buffer_size: The maximum size of a message when using the polling transport. :param allow_upgrades: Whether to allow transport upgrades or not. :param http_compression: Whether to compress packages when using the polling transport. :param compression_threshold: Only compress messages when their byte size is greater than this value. :param cookie: Name of the HTTP cookie that contains the client session id. If set to ``None``, a cookie is not sent to the client. :param cors_allowed_origins: List of origins that are allowed to connect to this server. All origins are allowed by default. :param cors_credentials: Whether credentials (cookies, authentication) are allowed in requests to this server. :param logger: To enable logging set to ``True`` or pass a logger object to use. To disable logging set to ``False``. :param json: An alternative json module to use for encoding and decoding packets. Custom json modules must have ``dumps`` and ``loads`` functions that are compatible with the standard library versions. :param kwargs: Reserved for future extensions, any additional parameters given as keyword arguments will be silently ignored. """ def is_asyncio_based(self): return True def async_modes(self): return ['aiohttp', 'sanic'] def attach(self, app, engineio_path='engine.io'): """Attach the Engine.IO server to an application.""" engineio_path = engineio_path.strip('/') self._async['create_route'](app, self, '/{}/'.format(engineio_path)) async def send(self, sid, data, binary=None): """Send a message to a client. :param sid: The session id of the recipient client. :param data: The data to send to the client. Data can be of type ``str``, ``bytes``, ``list`` or ``dict``. If a ``list`` or ``dict``, the data will be serialized as JSON. :param binary: ``True`` to send packet as binary, ``False`` to send as text. If not given, unicode (Python 2) and str (Python 3) are sent as text, and str (Python 2) and bytes (Python 3) are sent as binary. Note: this method is a coroutine. """ try: socket = self._get_socket(sid) except KeyError: # the socket is not available self.logger.warning('Cannot send to sid %s', sid) return await socket.send(packet.Packet(packet.MESSAGE, data=data, binary=binary)) async def disconnect(self, sid=None): """Disconnect a client. :param sid: The session id of the client to close. If this parameter is not given, then all clients are closed. Note: this method is a coroutine. """ if sid is not None: await self._get_socket(sid).close() del self.sockets[sid] else: await asyncio.wait([client.close() for client in six.itervalues(self.sockets)]) self.sockets = {} async def handle_request(self, *args, **kwargs): """Handle an HTTP request from the client. This is the entry point of the Engine.IO application. This function returns the HTTP response to deliver to the client. Note: this method is a coroutine. """ environ = self._async['translate_request'](*args, **kwargs) method = environ['REQUEST_METHOD'] query = urllib.parse.parse_qs(environ.get('QUERY_STRING', '')) if 'j' in query: self.logger.warning('JSONP requests are not supported') r = self._bad_request() else: sid = query['sid'][0] if 'sid' in query else None b64 = False if 'b64' in query: if query['b64'][0] == "1" or query['b64'][0].lower() == "true": b64 = True if method == 'GET': if sid is None: transport = query.get('transport', ['polling'])[0] if transport != 'polling' and transport != 'websocket': self.logger.warning('Invalid transport %s', transport) r = self._bad_request() else: r = await self._handle_connect(environ, transport, b64) else: if sid not in self.sockets: self.logger.warning('Invalid session %s', sid) r = self._bad_request() else: socket = self._get_socket(sid) try: packets = await socket.handle_get_request(environ) if isinstance(packets, list): r = self._ok(packets, b64=b64) else: r = packets except EngineIOError: if sid in self.sockets: # pragma: no cover await self.disconnect(sid) r = self._bad_request() if sid in self.sockets and self.sockets[sid].closed: del self.sockets[sid] elif method == 'POST': if sid is None or sid not in self.sockets: self.logger.warning('Invalid session %s', sid) r = self._bad_request() else: socket = self._get_socket(sid) try: await socket.handle_post_request(environ) r = self._ok() except EngineIOError: if sid in self.sockets: # pragma: no cover await self.disconnect(sid) r = self._bad_request() except: # pragma: no cover # for any other unexpected errors, we log the error # and keep going self.logger.exception('post request handler error') r = self._ok() else: self.logger.warning('Method %s not supported', method) r = self._method_not_found() if not isinstance(r, dict): return r or [] if self.http_compression and \ len(r['response']) >= self.compression_threshold: encodings = [e.split(';')[0].strip() for e in environ.get('HTTP_ACCEPT_ENCODING', '').split(',')] for encoding in encodings: if encoding in self.compression_methods: r['response'] = \ getattr(self, '_' + encoding)(r['response']) r['headers'] += [('Content-Encoding', encoding)] break cors_headers = self._cors_headers(environ) return self._async['make_response'](r['status'], r['headers'] + cors_headers, r['response']) def start_background_task(self, target, *args, **kwargs): """Start a background task using the appropriate async model. This is a utility function that applications can use to start a background task using the method that is compatible with the selected async mode. :param target: the target function to execute. :param args: arguments to pass to the function. :param kwargs: keyword arguments to pass to the function. The return value is a ``asyncio.Task`` object. """ return asyncio.ensure_future(target(*args, **kwargs)) async def sleep(self, seconds=0): """Sleep for the requested amount of time using the appropriate async model. This is a utility function that applications can use to put a task to sleep without having to worry about using the correct call for the selected async mode. Note: this method is a coroutine. """ return await asyncio.sleep(seconds) async def _handle_connect(self, environ, transport, b64=False): """Handle a client connection request.""" sid = self._generate_id() s = asyncio_socket.AsyncSocket(self, sid) self.sockets[sid] = s pkt = packet.Packet( packet.OPEN, {'sid': sid, 'upgrades': self._upgrades(sid, transport), 'pingTimeout': int(self.ping_timeout * 1000), 'pingInterval': int(self.ping_interval * 1000)}) await s.send(pkt) ret = await self._trigger_event('connect', sid, environ) if ret is False: del self.sockets[sid] self.logger.warning('Application rejected connection') return self._unauthorized() if transport == 'websocket': ret = await s.handle_get_request(environ) if s.closed: # websocket connection ended, so we are done del self.sockets[sid] return ret else: s.connected = True headers = None if self.cookie: headers = [('Set-Cookie', self.cookie + '=' + sid)] return self._ok(await s.poll(), headers=headers, b64=b64) async def _trigger_event(self, event, *args, **kwargs): """Invoke an event handler.""" ret = None if event in self.handlers: if asyncio.iscoroutinefunction(self.handlers[event]) is True: try: ret = await self.handlers[event](*args) except asyncio.CancelledError: # pragma: no cover pass except: self.logger.exception(event + ' async handler error') if event == 'connect': # if connect handler raised error we reject the # connection return False else: try: return self.handlers[event](*args) except: self.logger.exception(event + ' handler error') if event == 'connect': # if connect handler raised error we reject the # connection return False return ret python-engineio-1.6.1/engineio/asyncio_socket.py0000644_j0000002050313124366223022343 0ustar migu778100000000000000import asyncio import six import time from . import exceptions from . import packet from . import payload from . import socket class AsyncSocket(socket.Socket): def create_queue(self): return asyncio.Queue() async def poll(self): """Wait for packets to send to the client.""" try: packets = [await asyncio.wait_for(self.queue.get(), self.server.ping_timeout)] self.queue.task_done() except (asyncio.TimeoutError, asyncio.CancelledError): raise exceptions.QueueEmpty() if packets == [None]: return [] try: packets.append(self.queue.get_nowait()) self.queue.task_done() except asyncio.QueueEmpty: pass return packets async def receive(self, pkt): """Receive packet from the client.""" self.server.logger.info('%s: Received packet %s data %s', self.sid, packet.packet_names[pkt.packet_type], pkt.data if not isinstance(pkt.data, bytes) else '') if pkt.packet_type == packet.PING: self.last_ping = time.time() await self.send(packet.Packet(packet.PONG, pkt.data)) elif pkt.packet_type == packet.MESSAGE: await self.server._trigger_event('message', self.sid, pkt.data) elif pkt.packet_type == packet.UPGRADE: await self.send(packet.Packet(packet.NOOP)) elif pkt.packet_type == packet.CLOSE: await self.close(wait=False, abort=True) else: raise exceptions.UnknownPacketError() async def send(self, pkt): """Send a packet to the client.""" if self.closed: raise IOError('Socket is closed') if time.time() - self.last_ping > self.server.ping_timeout: self.server.logger.info('%s: Client is gone, closing socket', self.sid) return await self.close(wait=False, abort=True) self.server.logger.info('%s: Sending packet %s data %s', self.sid, packet.packet_names[pkt.packet_type], pkt.data if not isinstance(pkt.data, bytes) else '') await self.queue.put(pkt) async def handle_get_request(self, environ): """Handle a long-polling GET request from the client.""" connections = [ s.strip() for s in environ.get('HTTP_CONNECTION', '').lower().split(',')] transport = environ.get('HTTP_UPGRADE', '').lower() if 'upgrade' in connections and transport in self.upgrade_protocols: self.server.logger.info('%s: Received request to upgrade to %s', self.sid, transport) return await getattr(self, '_upgrade_' + transport)(environ) try: packets = await self.poll() except exceptions.QueueEmpty: await self.close(wait=False) raise return packets async def handle_post_request(self, environ): """Handle a long-polling POST request from the client.""" length = int(environ.get('CONTENT_LENGTH', '0')) if length > self.server.max_http_buffer_size: raise exceptions.ContentTooLongError() else: body = await environ['wsgi.input'].read(length) p = payload.Payload(encoded_payload=body) for pkt in p.packets: await self.receive(pkt) async def close(self, wait=True, abort=False): """Close the socket connection.""" if not self.closed and not self.closing: self.closing = True await self.server._trigger_event('disconnect', self.sid) if not abort: await self.send(packet.Packet(packet.CLOSE)) self.closed = True if wait: await self.queue.join() async def _upgrade_websocket(self, environ): """Upgrade the connection from polling to websocket.""" if self.upgraded: raise IOError('Socket has been upgraded already') if self.server._async['websocket'] is None or \ self.server._async['websocket_class'] is None: # the selected async mode does not support websocket return self.server._bad_request() websocket_class = getattr(self.server._async['websocket'], self.server._async['websocket_class']) ws = websocket_class(self._websocket_handler) return await ws(environ) async def _websocket_handler(self, ws): """Engine.IO handler for websocket transport.""" if self.connected: # the socket was already connected, so this is an upgrade await self.queue.join() # flush the queue first pkt = await ws.wait() if pkt != packet.Packet(packet.PING, data=six.text_type('probe')).encode( always_bytes=False): self.server.logger.info( '%s: Failed websocket upgrade, no PING packet', self.sid) return await ws.send(packet.Packet( packet.PONG, data=six.text_type('probe')).encode(always_bytes=False)) await self.send(packet.Packet(packet.NOOP)) pkt = await ws.wait() decoded_pkt = packet.Packet(encoded_packet=pkt) if decoded_pkt.packet_type != packet.UPGRADE: self.upgraded = False self.server.logger.info( ('%s: Failed websocket upgrade, expected UPGRADE packet, ' 'received %s instead.'), self.sid, pkt) return self.upgraded = True else: self.connected = True self.upgraded = True # start separate writer thread async def writer(): while True: packets = None try: packets = await self.poll() except exceptions.QueueEmpty: break if not packets: # empty packet list returned -> connection closed break try: for pkt in packets: await ws.send(pkt.encode(always_bytes=False)) except: break writer_task = asyncio.ensure_future(writer()) self.server.logger.info( '%s: Upgrade to websocket successful', self.sid) while True: p = None wait_task = asyncio.ensure_future(ws.wait()) try: p = await asyncio.wait_for(wait_task, self.server.ping_timeout) except asyncio.CancelledError: # pragma: no cover # there is a bug (https://bugs.python.org/issue30508) in # asyncio that causes a "Task exception never retrieved" error # to appear when wait_task raises an exception before it gets # cancelled. Calling wait_task.exception() prevents the error # from being issued in Python 3.6, but causes other errors in # other versions, so we run it with all errors suppressed and # hope for the best. try: wait_task.exception() except: pass break except: break if p is None: # connection closed by client break if isinstance(p, six.text_type): # pragma: no cover p = p.encode('utf-8') pkt = packet.Packet(encoded_packet=p) try: await self.receive(pkt) except exceptions.UnknownPacketError: pass except: # pragma: no cover # if we get an unexpected exception we log the error and exit # the connection properly self.server.logger.exception('Receive error') await self.queue.put(None) # unlock the writer task so it can exit await asyncio.wait_for(writer_task, timeout=None) await self.close(wait=True, abort=True) python-engineio-1.6.1/engineio/exceptions.py0000644_j0000000027613075332622021515 0ustar migu778100000000000000class EngineIOError(Exception): pass class ContentTooLongError(EngineIOError): pass class UnknownPacketError(EngineIOError): pass class QueueEmpty(EngineIOError): pass python-engineio-1.6.1/engineio/middleware.py0000644_j0000000425713071753705021461 0ustar migu778100000000000000class Middleware(object): """WSGI middleware for Engine.IO. This middleware dispatches traffic to an Engine.IO application, and optionally forwards regular HTTP traffic to a WSGI application. :param engineio_app: The Engine.IO server. :param wsgi_app: The WSGI app that receives all other traffic. :param engineio_path: The endpoint where the Engine.IO application should be installed. The default value is appropriate for most cases. Example usage:: import engineio import eventlet from . import wsgi_app eio = engineio.Server() app = engineio.Middleware(eio, wsgi_app) eventlet.wsgi.server(eventlet.listen(('', 8000)), app) """ def __init__(self, engineio_app, wsgi_app=None, engineio_path='engine.io'): self.engineio_app = engineio_app self.wsgi_app = wsgi_app self.engineio_path = engineio_path.strip('/') def __call__(self, environ, start_response): if 'gunicorn.socket' in environ: # gunicorn saves the socket under environ['gunicorn.socket'], while # eventlet saves it under environ['eventlet.input']. Eventlet also # stores the socket inside a wrapper class, while gunicon writes it # directly into the environment. To give eventlet's WebSocket # module access to this socket when running under gunicorn, here we # copy the socket to the eventlet format. class Input(object): def __init__(self, socket): self.socket = socket def get_socket(self): return self.socket environ['eventlet.input'] = Input(environ['gunicorn.socket']) path = environ['PATH_INFO'] if path is not None and \ path.startswith('/{0}/'.format(self.engineio_path)): return self.engineio_app.handle_request(environ, start_response) elif self.wsgi_app is not None: return self.wsgi_app(environ, start_response) else: start_response("404 Not Found", [('Content-type', 'text/plain')]) return ['Not Found'] python-engineio-1.6.1/engineio/packet.py0000644_j0000000622113037745742020610 0ustar migu778100000000000000import base64 import json as _json import six (OPEN, CLOSE, PING, PONG, MESSAGE, UPGRADE, NOOP) = (0, 1, 2, 3, 4, 5, 6) packet_names = ['OPEN', 'CLOSE', 'PING', 'PONG', 'MESSAGE', 'UPGRADE', 'NOOP'] binary_types = (six.binary_type, bytearray) class Packet(object): """Engine.IO packet.""" json = _json def __init__(self, packet_type=NOOP, data=None, binary=None, encoded_packet=None): self.packet_type = packet_type self.data = data if binary is not None: self.binary = binary elif isinstance(data, six.text_type): self.binary = False elif isinstance(data, binary_types): self.binary = True else: self.binary = False if encoded_packet: self.decode(encoded_packet) def encode(self, b64=False, always_bytes=True): """Encode the packet for transmission.""" if self.binary and not b64: encoded_packet = six.int2byte(self.packet_type) else: encoded_packet = six.text_type(self.packet_type) if self.binary and b64: encoded_packet = 'b' + encoded_packet if self.binary: if b64: encoded_packet += base64.b64encode(self.data).decode('utf-8') else: encoded_packet += self.data elif isinstance(self.data, six.string_types): encoded_packet += self.data elif isinstance(self.data, dict) or isinstance(self.data, list): encoded_packet += self.json.dumps(self.data, separators=(',', ':')) elif self.data is not None: encoded_packet += str(self.data) if always_bytes and not isinstance(encoded_packet, binary_types): encoded_packet = encoded_packet.encode('utf-8') return encoded_packet def decode(self, encoded_packet): """Decode a transmitted package.""" b64 = False if not isinstance(encoded_packet, binary_types): encoded_packet = encoded_packet.encode('utf-8') elif not isinstance(encoded_packet, bytes): encoded_packet = bytes(encoded_packet) self.packet_type = six.byte2int(encoded_packet[0:1]) if self.packet_type == 98: # 'b' --> binary base64 encoded packet self.binary = True encoded_packet = encoded_packet[1:] self.packet_type = six.byte2int(encoded_packet[0:1]) self.packet_type -= 48 b64 = True elif self.packet_type >= 48: self.packet_type -= 48 self.binary = False else: self.binary = True self.data = None if len(encoded_packet) > 1: if self.binary: if b64: self.data = base64.b64decode(encoded_packet[1:]) else: self.data = encoded_packet[1:] else: try: self.data = self.json.loads( encoded_packet[1:].decode('utf-8')) except ValueError: self.data = encoded_packet[1:].decode('utf-8') python-engineio-1.6.1/engineio/payload.py0000644_j0000000747113113602474020767 0ustar migu778100000000000000import six from . import packet class Payload(object): """Engine.IO payload.""" def __init__(self, packets=None, encoded_payload=None): self.packets = packets or [] if encoded_payload is not None: self.decode(encoded_payload) def encode(self, b64=False): """Encode the payload for transmission.""" encoded_payload = b'' for pkt in self.packets: encoded_packet = pkt.encode(b64=b64) packet_len = len(encoded_packet) if b64: encoded_payload += str(packet_len).encode('utf-8') + b':' + \ encoded_packet else: binary_len = b'' while packet_len != 0: binary_len = six.int2byte(packet_len % 10) + binary_len packet_len = int(packet_len / 10) if not pkt.binary: encoded_payload += b'\0' else: encoded_payload += b'\1' encoded_payload += binary_len + b'\xff' + encoded_packet return encoded_payload def decode(self, encoded_payload): """Decode a transmitted payload.""" fixed_double_encode = False self.packets = [] while encoded_payload: if six.byte2int(encoded_payload[0:1]) <= 1: packet_len = 0 i = 1 while six.byte2int(encoded_payload[i:i + 1]) != 255: packet_len = packet_len * 10 + six.byte2int( encoded_payload[i:i + 1]) i += 1 self.packets.append(packet.Packet( encoded_packet=encoded_payload[i + 1:i + 1 + packet_len])) else: i = encoded_payload.find(b':') if i == -1: raise ValueError('invalid payload') # the packet_len below is given in utf-8 characters, but we # receive the payload as bytes, so down below this length is # adjusted to reflect byte length packet_len = int(encoded_payload[0:i]) if not fixed_double_encode: # the engine.io javascript client sends text payloads with # a double UTF-8 encoding. Here we try to fix that mess and # restore the original packet try: # first we remove one UTF-8 encoding layer fixed_payload = encoded_payload.decode( 'utf-8').encode('raw_unicode_escape') # then we make sure the result can be decoded a second # time (this will raise an exception if not) fixed_payload.decode('utf-8') # if a second utf-8 decode worked, then this appears to # be a double encoded packet, so here we keep the # packet after a single decode, since the packet class # will perform a decode as well, and in this case it is # not necessary to adjust the packet length encoded_payload = fixed_payload except: # if we couldn't apply a double utf-8 decode then # the packet must have been correct, so we just adjust # the packet length to be in bytes and not utf-8 # characters and keep going packet_len += len(encoded_payload) - len(fixed_payload) fixed_double_encode = True pkt = encoded_payload[i + 1: i + 1 + packet_len] self.packets.append(packet.Packet(encoded_packet=pkt)) encoded_payload = encoded_payload[i + 1 + packet_len:] python-engineio-1.6.1/engineio/server.py0000644_j0000004731513124366223020646 0ustar migu778100000000000000import gzip import importlib import logging import uuid import zlib import six from six.moves import urllib from .exceptions import EngineIOError from . import packet from . import payload from . import socket default_logger = logging.getLogger('engineio') class Server(object): """An Engine.IO server. This class implements a fully compliant Engine.IO web server with support for websocket and long-polling transports. :param async_mode: The asynchronous model to use. See the Deployment section in the documentation for a description of the available options. Valid async modes are "threading", "eventlet", "gevent" and "gevent_uwsgi". If this argument is not given, "eventlet" is tried first, then "gevent_uwsgi", then "gevent", and finally "threading". The first async mode that has all its dependencies installed is then one that is chosen. :param ping_timeout: The time in seconds that the client waits for the server to respond before disconnecting. :param ping_interval: The interval in seconds at which the client pings the server. :param max_http_buffer_size: The maximum size of a message when using the polling transport. :param allow_upgrades: Whether to allow transport upgrades or not. :param http_compression: Whether to compress packages when using the polling transport. :param compression_threshold: Only compress messages when their byte size is greater than this value. :param cookie: Name of the HTTP cookie that contains the client session id. If set to ``None``, a cookie is not sent to the client. :param cors_allowed_origins: List of origins that are allowed to connect to this server. All origins are allowed by default. :param cors_credentials: Whether credentials (cookies, authentication) are allowed in requests to this server. :param logger: To enable logging set to ``True`` or pass a logger object to use. To disable logging set to ``False``. :param json: An alternative json module to use for encoding and decoding packets. Custom json modules must have ``dumps`` and ``loads`` functions that are compatible with the standard library versions. :param async_handlers: If set to ``True``, run message event handlers in non-blocking threads. To run handlers synchronously, set to ``False``. The default is ``True``. :param kwargs: Reserved for future extensions, any additional parameters given as keyword arguments will be silently ignored. """ compression_methods = ['gzip', 'deflate'] event_names = ['connect', 'disconnect', 'message'] def __init__(self, async_mode=None, ping_timeout=60, ping_interval=25, max_http_buffer_size=100000000, allow_upgrades=True, http_compression=True, compression_threshold=1024, cookie='io', cors_allowed_origins=None, cors_credentials=True, logger=False, json=None, async_handlers=True, **kwargs): self.ping_timeout = ping_timeout self.ping_interval = ping_interval self.max_http_buffer_size = max_http_buffer_size self.allow_upgrades = allow_upgrades self.http_compression = http_compression self.compression_threshold = compression_threshold self.cookie = cookie self.cors_allowed_origins = cors_allowed_origins self.cors_credentials = cors_credentials self.async_handlers = async_handlers self.sockets = {} self.handlers = {} if json is not None: packet.Packet.json = json if not isinstance(logger, bool): self.logger = logger else: self.logger = default_logger if not logging.root.handlers and \ self.logger.level == logging.NOTSET: if logger: self.logger.setLevel(logging.INFO) else: self.logger.setLevel(logging.ERROR) self.logger.addHandler(logging.StreamHandler()) if async_mode is None: modes = self.async_modes() else: modes = [async_mode] self._async = None self.async_mode = None for mode in modes: try: self._async = importlib.import_module( 'engineio.async_' + mode)._async asyncio_based = self._async['asyncio'] \ if 'asyncio' in self._async else False if asyncio_based != self.is_asyncio_based(): continue self.async_mode = mode break except ImportError: pass if self.async_mode is None: raise ValueError('Invalid async_mode specified') if self.is_asyncio_based() and \ ('asyncio' not in self._async or not self._async['asyncio']): # pragma: no cover raise ValueError('The selected async_mode is not asyncio ' 'compatible') if not self.is_asyncio_based() and 'asyncio' in self._async and \ self._async['asyncio']: # pragma: no cover raise ValueError('The selected async_mode requires asyncio and ' 'must use the AsyncServer class') self.logger.info('Server initialized for %s.', self.async_mode) def is_asyncio_based(self): return False def async_modes(self): return ['eventlet', 'gevent_uwsgi', 'gevent', 'threading'] def on(self, event, handler=None): """Register an event handler. :param event: The event name. Can be ``'connect'``, ``'message'`` or ``'disconnect'``. :param handler: The function that should be invoked to handle the event. When this parameter is not given, the method acts as a decorator for the handler function. Example usage:: # as a decorator: @eio.on('connect') def connect_handler(sid, environ): print('Connection request') if environ['REMOTE_ADDR'] in blacklisted: return False # reject # as a method: def message_handler(sid, msg): print('Received message: ', msg) eio.send(sid, 'response') eio.on('message', message_handler) The handler function receives the ``sid`` (session ID) for the client as first argument. The ``'connect'`` event handler receives the WSGI environment as a second argument, and can return ``False`` to reject the connection. The ``'message'`` handler receives the message payload as a second argument. The ``'disconnect'`` handler does not take a second argument. """ if event not in self.event_names: raise ValueError('Invalid event') def set_handler(handler): self.handlers[event] = handler return handler if handler is None: return set_handler set_handler(handler) def send(self, sid, data, binary=None): """Send a message to a client. :param sid: The session id of the recipient client. :param data: The data to send to the client. Data can be of type ``str``, ``bytes``, ``list`` or ``dict``. If a ``list`` or ``dict``, the data will be serialized as JSON. :param binary: ``True`` to send packet as binary, ``False`` to send as text. If not given, unicode (Python 2) and str (Python 3) are sent as text, and str (Python 2) and bytes (Python 3) are sent as binary. """ try: socket = self._get_socket(sid) except KeyError: # the socket is not available self.logger.warning('Cannot send to sid %s', sid) return socket.send(packet.Packet(packet.MESSAGE, data=data, binary=binary)) def disconnect(self, sid=None): """Disconnect a client. :param sid: The session id of the client to close. If this parameter is not given, then all clients are closed. """ if sid is not None: self._get_socket(sid).close() del self.sockets[sid] else: for client in six.itervalues(self.sockets): client.close() self.sockets = {} def transport(self, sid): """Return the name of the transport used by the client. The two possible values returned by this function are ``'polling'`` and ``'websocket'``. :param sid: The session of the client. """ return 'websocket' if self._get_socket(sid).upgraded else 'polling' def handle_request(self, environ, start_response): """Handle an HTTP request from the client. This is the entry point of the Engine.IO application, using the same interface as a WSGI application. For the typical usage, this function is invoked by the :class:`Middleware` instance, but it can be invoked directly when the middleware is not used. :param environ: The WSGI environment. :param start_response: The WSGI ``start_response`` function. This function returns the HTTP response body to deliver to the client as a byte sequence. """ method = environ['REQUEST_METHOD'] query = urllib.parse.parse_qs(environ.get('QUERY_STRING', '')) if 'j' in query: self.logger.warning('JSONP requests are not supported') r = self._bad_request() else: sid = query['sid'][0] if 'sid' in query else None b64 = False if 'b64' in query: if query['b64'][0] == "1" or query['b64'][0].lower() == "true": b64 = True if method == 'GET': if sid is None: transport = query.get('transport', ['polling'])[0] if transport != 'polling' and transport != 'websocket': self.logger.warning('Invalid transport %s', transport) r = self._bad_request() else: r = self._handle_connect(environ, start_response, transport, b64) else: if sid not in self.sockets: self.logger.warning('Invalid session %s', sid) r = self._bad_request() else: socket = self._get_socket(sid) try: packets = socket.handle_get_request( environ, start_response) if isinstance(packets, list): r = self._ok(packets, b64=b64) else: r = packets except EngineIOError: if sid in self.sockets: # pragma: no cover self.disconnect(sid) r = self._bad_request() if sid in self.sockets and self.sockets[sid].closed: del self.sockets[sid] elif method == 'POST': if sid is None or sid not in self.sockets: self.logger.warning('Invalid session %s', sid) r = self._bad_request() else: socket = self._get_socket(sid) try: socket.handle_post_request(environ) r = self._ok() except EngineIOError: if sid in self.sockets: # pragma: no cover self.disconnect(sid) r = self._bad_request() except: # pragma: no cover # for any other unexpected errors, we log the error # and keep going self.logger.exception('post request handler error') r = self._ok() else: self.logger.warning('Method %s not supported', method) r = self._method_not_found() if not isinstance(r, dict): return r or [] if self.http_compression and \ len(r['response']) >= self.compression_threshold: encodings = [e.split(';')[0].strip() for e in environ.get('HTTP_ACCEPT_ENCODING', '').split(',')] for encoding in encodings: if encoding in self.compression_methods: r['response'] = \ getattr(self, '_' + encoding)(r['response']) r['headers'] += [('Content-Encoding', encoding)] break cors_headers = self._cors_headers(environ) start_response(r['status'], r['headers'] + cors_headers) return [r['response']] def start_background_task(self, target, *args, **kwargs): """Start a background task using the appropriate async model. This is a utility function that applications can use to start a background task using the method that is compatible with the selected async mode. :param target: the target function to execute. :param args: arguments to pass to the function. :param kwargs: keyword arguments to pass to the function. This function returns an object compatible with the `Thread` class in the Python standard library. The `start()` method on this object is already called by this function. """ th = getattr(self._async['threading'], self._async['thread_class'])(target=target, args=args, kwargs=kwargs) th.start() return th # pragma: no cover def sleep(self, seconds=0): """Sleep for the requested amount of time using the appropriate async model. This is a utility function that applications can use to put a task to sleep without having to worry about using the correct call for the selected async mode. """ return self._async['sleep'](seconds) def _generate_id(self): """Generate a unique session id.""" return uuid.uuid4().hex def _handle_connect(self, environ, start_response, transport, b64=False): """Handle a client connection request.""" sid = self._generate_id() s = socket.Socket(self, sid) self.sockets[sid] = s pkt = packet.Packet( packet.OPEN, {'sid': sid, 'upgrades': self._upgrades(sid, transport), 'pingTimeout': int(self.ping_timeout * 1000), 'pingInterval': int(self.ping_interval * 1000)}) s.send(pkt) ret = self._trigger_event('connect', sid, environ, async=False) if ret is False: del self.sockets[sid] self.logger.warning('Application rejected connection') return self._unauthorized() if transport == 'websocket': ret = s.handle_get_request(environ, start_response) if s.closed: # websocket connection ended, so we are done del self.sockets[sid] return ret else: s.connected = True headers = None if self.cookie: headers = [('Set-Cookie', self.cookie + '=' + sid)] return self._ok(s.poll(), headers=headers, b64=b64) def _upgrades(self, sid, transport): """Return the list of possible upgrades for a client connection.""" if not self.allow_upgrades or self._get_socket(sid).upgraded or \ self._async['websocket_class'] is None or \ transport == 'websocket': return [] return ['websocket'] def _trigger_event(self, event, *args, **kwargs): """Invoke an event handler.""" async = kwargs.pop('async', False) if event in self.handlers: if async: return self.start_background_task(self.handlers[event], *args) else: try: return self.handlers[event](*args) except: self.logger.exception(event + ' handler error') if event == 'connect': # if connect handler raised error we reject the # connection return False def _get_socket(self, sid): """Return the socket object for a given session.""" try: s = self.sockets[sid] except KeyError: raise KeyError('Session not found') if s.closed: del self.sockets[sid] raise KeyError('Session is disconnected') return s def _ok(self, packets=None, headers=None, b64=False): """Generate a successful HTTP response.""" if packets is not None: if headers is None: headers = [] if b64: headers += [('Content-Type', 'text/plain; charset=UTF-8')] else: headers += [('Content-Type', 'application/octet-stream')] return {'status': '200 OK', 'headers': headers, 'response': payload.Payload(packets=packets).encode(b64)} else: return {'status': '200 OK', 'headers': [('Content-Type', 'text/plain')], 'response': b'OK'} def _bad_request(self): """Generate a bad request HTTP error response.""" return {'status': '400 BAD REQUEST', 'headers': [('Content-Type', 'text/plain')], 'response': b'Bad Request'} def _method_not_found(self): """Generate a method not found HTTP error response.""" return {'status': '405 METHOD NOT FOUND', 'headers': [('Content-Type', 'text/plain')], 'response': b'Method Not Found'} def _unauthorized(self): """Generate a unauthorized HTTP error response.""" return {'status': '401 UNAUTHORIZED', 'headers': [('Content-Type', 'text/plain')], 'response': b'Unauthorized'} def _cors_headers(self, environ): """Return the cross-origin-resource-sharing headers.""" if self.cors_allowed_origins is not None and \ environ.get('HTTP_ORIGIN', '') not in \ self.cors_allowed_origins: return [] if 'HTTP_ORIGIN' in environ: headers = [('Access-Control-Allow-Origin', environ['HTTP_ORIGIN'])] else: headers = [('Access-Control-Allow-Origin', '*')] if self.cors_credentials: headers += [('Access-Control-Allow-Credentials', 'true')] return headers def _gzip(self, response): """Apply gzip compression to a response.""" bytesio = six.BytesIO() with gzip.GzipFile(fileobj=bytesio, mode='w') as gz: gz.write(response) return bytesio.getvalue() def _deflate(self, response): """Apply deflate compression to a response.""" return zlib.compress(response) python-engineio-1.6.1/engineio/socket.py0000644_j0000002120213124366223020613 0ustar migu778100000000000000import six import time from . import exceptions from . import packet from . import payload class Socket(object): """An Engine.IO socket.""" upgrade_protocols = ['websocket'] def __init__(self, server, sid): self.server = server self.sid = sid self.queue = self.create_queue() self.last_ping = time.time() self.connected = False self.upgraded = False self.closing = False self.closed = False def create_queue(self): return getattr(self.server._async['queue'], self.server._async['queue_class'])() def poll(self): """Wait for packets to send to the client.""" try: packets = [self.queue.get(timeout=self.server.ping_timeout)] self.queue.task_done() except self.server._async['queue'].Empty: raise exceptions.QueueEmpty() if packets == [None]: return [] try: packets.append(self.queue.get(block=False)) self.queue.task_done() except self.server._async['queue'].Empty: pass return packets def receive(self, pkt): """Receive packet from the client.""" packet_name = packet.packet_names[pkt.packet_type] \ if pkt.packet_type < len(packet.packet_names) else 'UNKNOWN' self.server.logger.info('%s: Received packet %s data %s', self.sid, packet_name, pkt.data if not isinstance(pkt.data, bytes) else '') if pkt.packet_type == packet.PING: self.last_ping = time.time() self.send(packet.Packet(packet.PONG, pkt.data)) elif pkt.packet_type == packet.MESSAGE: self.server._trigger_event('message', self.sid, pkt.data, async=self.server.async_handlers) elif pkt.packet_type == packet.UPGRADE: self.send(packet.Packet(packet.NOOP)) elif pkt.packet_type == packet.CLOSE: self.close(wait=False, abort=True) else: raise exceptions.UnknownPacketError() def send(self, pkt): """Send a packet to the client.""" if self.closed: raise IOError('Socket is closed') if time.time() - self.last_ping > self.server.ping_timeout: self.server.logger.info('%s: Client is gone, closing socket', self.sid) self.close(wait=False, abort=True) return self.queue.put(pkt) self.server.logger.info('%s: Sending packet %s data %s', self.sid, packet.packet_names[pkt.packet_type], pkt.data if not isinstance(pkt.data, bytes) else '') def handle_get_request(self, environ, start_response): """Handle a long-polling GET request from the client.""" connections = [ s.strip() for s in environ.get('HTTP_CONNECTION', '').lower().split(',')] transport = environ.get('HTTP_UPGRADE', '').lower() if 'upgrade' in connections and transport in self.upgrade_protocols: self.server.logger.info('%s: Received request to upgrade to %s', self.sid, transport) return getattr(self, '_upgrade_' + transport)(environ, start_response) try: packets = self.poll() except exceptions.QueueEmpty: self.close(wait=False) raise return packets def handle_post_request(self, environ): """Handle a long-polling POST request from the client.""" length = int(environ.get('CONTENT_LENGTH', '0')) if length > self.server.max_http_buffer_size: raise exceptions.ContentTooLongError() else: body = environ['wsgi.input'].read(length) p = payload.Payload(encoded_payload=body) for pkt in p.packets: self.receive(pkt) def close(self, wait=True, abort=False): """Close the socket connection.""" if not self.closed and not self.closing: self.closing = True self.server._trigger_event('disconnect', self.sid, async=False) if not abort: self.send(packet.Packet(packet.CLOSE)) self.closed = True if wait: self.queue.join() def _upgrade_websocket(self, environ, start_response): """Upgrade the connection from polling to websocket.""" if self.upgraded: raise IOError('Socket has been upgraded already') if self.server._async['websocket'] is None or \ self.server._async['websocket_class'] is None: # the selected async mode does not support websocket return self.server._bad_request() websocket_class = getattr(self.server._async['websocket'], self.server._async['websocket_class']) ws = websocket_class(self._websocket_handler) return ws(environ, start_response) def _websocket_handler(self, ws): """Engine.IO handler for websocket transport.""" # try to set a socket timeout matching the configured ping interval for attr in ['_sock', 'socket']: # pragma: no cover if hasattr(ws, attr) and hasattr(getattr(ws, attr), 'settimeout'): getattr(ws, attr).settimeout(self.server.ping_timeout) if self.connected: # the socket was already connected, so this is an upgrade self.queue.join() # flush the queue first pkt = ws.wait() if pkt != packet.Packet(packet.PING, data=six.text_type('probe')).encode( always_bytes=False): self.server.logger.info( '%s: Failed websocket upgrade, no PING packet', self.sid) return [] ws.send(packet.Packet( packet.PONG, data=six.text_type('probe')).encode(always_bytes=False)) self.send(packet.Packet(packet.NOOP)) pkt = ws.wait() decoded_pkt = packet.Packet(encoded_packet=pkt) if decoded_pkt.packet_type != packet.UPGRADE: self.upgraded = False self.server.logger.info( ('%s: Failed websocket upgrade, expected UPGRADE packet, ' 'received %s instead.'), self.sid, pkt) return [] self.upgraded = True else: self.connected = True self.upgraded = True # start separate writer thread def writer(): while True: packets = None try: packets = self.poll() except exceptions.QueueEmpty: break if not packets: # empty packet list returned -> connection closed break try: for pkt in packets: ws.send(pkt.encode(always_bytes=False)) except: break writer_task = self.server.start_background_task(writer) self.server.logger.info( '%s: Upgrade to websocket successful', self.sid) while True: p = None try: p = ws.wait() except Exception as e: # if the socket is already closed, we can assume this is a # downstream error of that if not self.closed: # pragma: no cover self.server.logger.info( '%s: Unexpected error "%s", closing connection', self.sid, str(e)) break if p is None: # connection closed by client break if isinstance(p, six.text_type): # pragma: no cover p = p.encode('utf-8') pkt = packet.Packet(encoded_packet=p) try: self.receive(pkt) except exceptions.UnknownPacketError: pass except: # pragma: no cover # if we get an unexpected exception we log the error and exit # the connection properly self.server.logger.exception('Receive error') break self.queue.put(None) # unlock the writer task so that it can exit writer_task.join() self.close(wait=True, abort=True) return [] python-engineio-1.6.1/LICENSE0000644_j0000000207213032743373016170 0ustar migu778100000000000000The MIT License (MIT) Copyright (c) 2015 Miguel Grinberg Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. python-engineio-1.6.1/MANIFEST.in0000644_j0000000003213032743373016713 0ustar migu778100000000000000include README.md LICENSE python-engineio-1.6.1/PKG-INFO0000644_j0000001213213124366231016252 0ustar migu778100000000000000Metadata-Version: 1.1 Name: python-engineio Version: 1.6.1 Summary: Engine.IO server Home-page: http://github.com/miguelgrinberg/python-engineio/ Author: Miguel Grinberg Author-email: miguelgrinberg50@gmail.com License: MIT Description: python-engineio =============== .. image:: https://travis-ci.org/miguelgrinberg/python-engineio.svg?branch=master :target: https://travis-ci.org/miguelgrinberg/python-engineio Python implementation of the `Engine.IO`_ realtime server. Features -------- - Fully compatible with the Javascript `engine.io-client`_ library, versions 1.5.0 and up. - Compatible with Python 2.7 and Python 3.3+. - Supports large number of clients even on modest hardware when used with an asynchronous server based on `asyncio`_(`sanic`_ or `aiohttp`_), `eventlet`_ or `gevent`_. For development and testing, any WSGI compliant multi-threaded server can be used. - Includes a WSGI middleware that integrates Engine.IO traffic with standard WSGI applications. - Uses an event-based architecture implemented with decorators that hides the details of the protocol. - Implements HTTP long-polling and WebSocket transports. - Supports XHR2 and XHR browsers as clients. - Supports text and binary messages. - Supports gzip and deflate HTTP compression. - Configurable CORS responses to avoid cross-origin problems with browsers. Examples -------- The following application uses the Eventlet asynchronous server, and includes a small Flask application that serves the HTML/Javascript to the client: .. code:: python import engineio import eventlet import eventlet.wsgi from flask import Flask, render_template eio = engineio.Server() app = Flask(__name__) @app.route('/') def index(): """Serve the client-side application.""" return render_template('index.html') @eio.on('connect') def connect(sid, environ): print("connect ", sid) @eio.on('message') def message(sid, data): print("message ", data) eio.send(sid, 'reply') @eio.on('disconnect') def disconnect(sid): print('disconnect ', sid) if __name__ == '__main__': # wrap Flask application with engineio's middleware app = engineio.Middleware(eio, app) # deploy as an eventlet WSGI server eventlet.wsgi.server(eventlet.listen(('', 8000)), app) And below is a similar example, coded for asyncio (Python 3.5+ only) with the `aiohttp`_ framework: .. code:: python from aiohttp import web import engineio eio = engineio.AsyncServer() app = web.Application() eio.attach(app) async def index(request): """Serve the client-side application.""" with open('index.html') as f: return web.Response(text=f.read(), content_type='text/html') @eio.on('connect') def connect(sid, environ): print("connect ", sid) @eio.on('message') async def message(sid, data): print("message ", data) await eio.send(sid, 'reply') @eio.on('disconnect') def disconnect(sid): print('disconnect ', sid) app.router.add_static('/static', 'static') app.router.add_get('/', index) if __name__ == '__main__': web.run_app(app) Resources --------- - `Documentation`_ - `PyPI`_ .. _Engine.IO: https://github.com/Automattic/engine.io .. _engine.io-client: https://github.com/Automattic/engine.io-client .. _asyncio: https://docs.python.org/3/library/asyncio.html .. _sanic: http://sanic.readthedocs.io/ .. _aiohttp: http://aiohttp.readthedocs.io/ .. _eventlet: http://eventlet.net/ .. _gevent: http://gevent.org/ .. _aiohttp: http://aiohttp.readthedocs.io/ .. _Documentation: http://pythonhosted.org/python-engineio .. _PyPI: https://pypi.python.org/pypi/python-engineio Platform: any Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules python-engineio-1.6.1/python_engineio.egg-info/0000755_j0000000000013124366231022046 5ustar migu778100000000000000python-engineio-1.6.1/python_engineio.egg-info/dependency_links.txt0000644_j0000000000113124366231026114 0ustar migu778100000000000000 python-engineio-1.6.1/python_engineio.egg-info/not-zip-safe0000644_j0000000000113032743721024275 0ustar migu778100000000000000 python-engineio-1.6.1/python_engineio.egg-info/PKG-INFO0000644_j0000001213213124366231023142 0ustar migu778100000000000000Metadata-Version: 1.1 Name: python-engineio Version: 1.6.1 Summary: Engine.IO server Home-page: http://github.com/miguelgrinberg/python-engineio/ Author: Miguel Grinberg Author-email: miguelgrinberg50@gmail.com License: MIT Description: python-engineio =============== .. image:: https://travis-ci.org/miguelgrinberg/python-engineio.svg?branch=master :target: https://travis-ci.org/miguelgrinberg/python-engineio Python implementation of the `Engine.IO`_ realtime server. Features -------- - Fully compatible with the Javascript `engine.io-client`_ library, versions 1.5.0 and up. - Compatible with Python 2.7 and Python 3.3+. - Supports large number of clients even on modest hardware when used with an asynchronous server based on `asyncio`_(`sanic`_ or `aiohttp`_), `eventlet`_ or `gevent`_. For development and testing, any WSGI compliant multi-threaded server can be used. - Includes a WSGI middleware that integrates Engine.IO traffic with standard WSGI applications. - Uses an event-based architecture implemented with decorators that hides the details of the protocol. - Implements HTTP long-polling and WebSocket transports. - Supports XHR2 and XHR browsers as clients. - Supports text and binary messages. - Supports gzip and deflate HTTP compression. - Configurable CORS responses to avoid cross-origin problems with browsers. Examples -------- The following application uses the Eventlet asynchronous server, and includes a small Flask application that serves the HTML/Javascript to the client: .. code:: python import engineio import eventlet import eventlet.wsgi from flask import Flask, render_template eio = engineio.Server() app = Flask(__name__) @app.route('/') def index(): """Serve the client-side application.""" return render_template('index.html') @eio.on('connect') def connect(sid, environ): print("connect ", sid) @eio.on('message') def message(sid, data): print("message ", data) eio.send(sid, 'reply') @eio.on('disconnect') def disconnect(sid): print('disconnect ', sid) if __name__ == '__main__': # wrap Flask application with engineio's middleware app = engineio.Middleware(eio, app) # deploy as an eventlet WSGI server eventlet.wsgi.server(eventlet.listen(('', 8000)), app) And below is a similar example, coded for asyncio (Python 3.5+ only) with the `aiohttp`_ framework: .. code:: python from aiohttp import web import engineio eio = engineio.AsyncServer() app = web.Application() eio.attach(app) async def index(request): """Serve the client-side application.""" with open('index.html') as f: return web.Response(text=f.read(), content_type='text/html') @eio.on('connect') def connect(sid, environ): print("connect ", sid) @eio.on('message') async def message(sid, data): print("message ", data) await eio.send(sid, 'reply') @eio.on('disconnect') def disconnect(sid): print('disconnect ', sid) app.router.add_static('/static', 'static') app.router.add_get('/', index) if __name__ == '__main__': web.run_app(app) Resources --------- - `Documentation`_ - `PyPI`_ .. _Engine.IO: https://github.com/Automattic/engine.io .. _engine.io-client: https://github.com/Automattic/engine.io-client .. _asyncio: https://docs.python.org/3/library/asyncio.html .. _sanic: http://sanic.readthedocs.io/ .. _aiohttp: http://aiohttp.readthedocs.io/ .. _eventlet: http://eventlet.net/ .. _gevent: http://gevent.org/ .. _aiohttp: http://aiohttp.readthedocs.io/ .. _Documentation: http://pythonhosted.org/python-engineio .. _PyPI: https://pypi.python.org/pypi/python-engineio Platform: any Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules python-engineio-1.6.1/python_engineio.egg-info/requires.txt0000644_j0000000001313124366231024440 0ustar migu778100000000000000six>=1.9.0 python-engineio-1.6.1/python_engineio.egg-info/SOURCES.txt0000644_j0000000116613124366231023736 0ustar migu778100000000000000LICENSE MANIFEST.in README.rst setup.py engineio/__init__.py engineio/async_aiohttp.py engineio/async_eventlet.py engineio/async_gevent.py engineio/async_gevent_uwsgi.py engineio/async_sanic.py engineio/async_threading.py engineio/asyncio_server.py engineio/asyncio_socket.py engineio/exceptions.py engineio/middleware.py engineio/packet.py engineio/payload.py engineio/server.py engineio/socket.py python_engineio.egg-info/PKG-INFO python_engineio.egg-info/SOURCES.txt python_engineio.egg-info/dependency_links.txt python_engineio.egg-info/not-zip-safe python_engineio.egg-info/requires.txt python_engineio.egg-info/top_level.txtpython-engineio-1.6.1/python_engineio.egg-info/top_level.txt0000644_j0000000001113124366231024570 0ustar migu778100000000000000engineio python-engineio-1.6.1/README.rst0000644_j0000000674413057715057016671 0ustar migu778100000000000000python-engineio =============== .. image:: https://travis-ci.org/miguelgrinberg/python-engineio.svg?branch=master :target: https://travis-ci.org/miguelgrinberg/python-engineio Python implementation of the `Engine.IO`_ realtime server. Features -------- - Fully compatible with the Javascript `engine.io-client`_ library, versions 1.5.0 and up. - Compatible with Python 2.7 and Python 3.3+. - Supports large number of clients even on modest hardware when used with an asynchronous server based on `asyncio`_(`sanic`_ or `aiohttp`_), `eventlet`_ or `gevent`_. For development and testing, any WSGI compliant multi-threaded server can be used. - Includes a WSGI middleware that integrates Engine.IO traffic with standard WSGI applications. - Uses an event-based architecture implemented with decorators that hides the details of the protocol. - Implements HTTP long-polling and WebSocket transports. - Supports XHR2 and XHR browsers as clients. - Supports text and binary messages. - Supports gzip and deflate HTTP compression. - Configurable CORS responses to avoid cross-origin problems with browsers. Examples -------- The following application uses the Eventlet asynchronous server, and includes a small Flask application that serves the HTML/Javascript to the client: .. code:: python import engineio import eventlet import eventlet.wsgi from flask import Flask, render_template eio = engineio.Server() app = Flask(__name__) @app.route('/') def index(): """Serve the client-side application.""" return render_template('index.html') @eio.on('connect') def connect(sid, environ): print("connect ", sid) @eio.on('message') def message(sid, data): print("message ", data) eio.send(sid, 'reply') @eio.on('disconnect') def disconnect(sid): print('disconnect ', sid) if __name__ == '__main__': # wrap Flask application with engineio's middleware app = engineio.Middleware(eio, app) # deploy as an eventlet WSGI server eventlet.wsgi.server(eventlet.listen(('', 8000)), app) And below is a similar example, coded for asyncio (Python 3.5+ only) with the `aiohttp`_ framework: .. code:: python from aiohttp import web import engineio eio = engineio.AsyncServer() app = web.Application() eio.attach(app) async def index(request): """Serve the client-side application.""" with open('index.html') as f: return web.Response(text=f.read(), content_type='text/html') @eio.on('connect') def connect(sid, environ): print("connect ", sid) @eio.on('message') async def message(sid, data): print("message ", data) await eio.send(sid, 'reply') @eio.on('disconnect') def disconnect(sid): print('disconnect ', sid) app.router.add_static('/static', 'static') app.router.add_get('/', index) if __name__ == '__main__': web.run_app(app) Resources --------- - `Documentation`_ - `PyPI`_ .. _Engine.IO: https://github.com/Automattic/engine.io .. _engine.io-client: https://github.com/Automattic/engine.io-client .. _asyncio: https://docs.python.org/3/library/asyncio.html .. _sanic: http://sanic.readthedocs.io/ .. _aiohttp: http://aiohttp.readthedocs.io/ .. _eventlet: http://eventlet.net/ .. _gevent: http://gevent.org/ .. _aiohttp: http://aiohttp.readthedocs.io/ .. _Documentation: http://pythonhosted.org/python-engineio .. _PyPI: https://pypi.python.org/pypi/python-engineio python-engineio-1.6.1/setup.cfg0000644_j0000000007313124366231016777 0ustar migu778100000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 python-engineio-1.6.1/setup.py0000755_j0000000247213032743373016704 0ustar migu778100000000000000""" python-engineio --------------- Engine.IO server. """ import re from setuptools import setup with open('engineio/__init__.py', 'r') as f: version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE).group(1) with open('README.rst', 'r') as f: long_description = f.read() setup( name='python-engineio', version=version, url='http://github.com/miguelgrinberg/python-engineio/', license='MIT', author='Miguel Grinberg', author_email='miguelgrinberg50@gmail.com', description='Engine.IO server', long_description=long_description, packages=['engineio'], zip_safe=False, include_package_data=True, platforms='any', install_requires=[ 'six>=1.9.0', ], tests_require=[ 'mock', 'eventlet', ], test_suite='tests', classifiers=[ 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules' ] )