django-websocket-redis-0.4.7/0000755000076500000240000000000013000363644016275 5ustar jriefstaff00000000000000django-websocket-redis-0.4.7/django_websocket_redis.egg-info/0000755000076500000240000000000013000363644024465 5ustar jriefstaff00000000000000django-websocket-redis-0.4.7/django_websocket_redis.egg-info/dependency_links.txt0000644000076500000240000000000113000363644030533 0ustar jriefstaff00000000000000 django-websocket-redis-0.4.7/django_websocket_redis.egg-info/not-zip-safe0000644000076500000240000000000112636050204026712 0ustar jriefstaff00000000000000 django-websocket-redis-0.4.7/django_websocket_redis.egg-info/PKG-INFO0000644000076500000240000001111613000363644025562 0ustar jriefstaff00000000000000Metadata-Version: 1.1 Name: django-websocket-redis Version: 0.4.7 Summary: Websocket support for Django using Redis as datastore Home-page: https://github.com/jrief/django-websocket-redis Author: Jacob Rief Author-email: jacob.rief@gmail.com License: MIT Description: django-websocket-redis ====================== Project home: https://github.com/jrief/django-websocket-redis Detailed documentation on [ReadTheDocs](http://django-websocket-redis.readthedocs.org/en/latest/). Online demo: http://django-websocket-redis.awesto.com/ Websockets for Django using Redis as message queue -------------------------------------------------- This module implements websockets on top of Django without requiring any additional framework. For messaging it uses the [Redis](http://redis.io/) datastore and in a production environment, it is intended to work under [uWSGI](http://projects.unbit.it/uwsgi/) and behind [NGiNX](http://nginx.com/) or [Apache](http://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html) version 2.4.5 or later. New in 0.4.5 ------------ * Created 1 requirements file under ``examples/chatserver/requirements.txt``. * Renamed chatclient.py to test_chatclient.py - for django-nose testrunner. * Migrated example project to django 1.7. * Edited ``docs/testing.rst`` to show new changes for using example project. * Added support for Python 3.3 and 3.4. * Added support for Django-1.8 * Removes the check for middleware by name. Features -------- * Largely scalable for Django applications with many hundreds of open websocket connections. * Runs a seperate Django main loop in a cooperative concurrency model using [gevent](http://www.gevent.org/), thus only one thread/process is required to control *all* open websockets simultaneously. * Full control over this seperate main loop during development, so **Django** can be started as usual with ``./manage.py runserver``. * No dependency to any other asynchronous event driven framework, such as Tornado, Twisted or Socket.io/Node.js. * Normal Django requests communicate with this seperate main loop through **Redis** which, by the way is a good replacement for memcached. * Optionally persiting messages, allowing server reboots and client reconnections. If unsure, if this proposed architecture is the correct approach on how to integrate Websockets with Django, then please read Roberto De Ioris (BDFL of uWSGI) article about [Offloading Websockets and Server-Sent Events AKA “Combine them with Django safely”](http://uwsgi-docs.readthedocs.org/en/latest/articles/OffloadingWebsocketsAndSSE.html). Please also consider, that whichever alternative technology you use, you always need a message queue, so that the Django application can “talk” to the browser. This is because the only link between the browser and the server is through the Websocket and thus, by definition a long living connection. For scalability reasons you can't start a Django server thread for each of these connections. Build status ------------ [![Build Status](https://travis-ci.org/jrief/django-websocket-redis.png?branch=master)](https://travis-ci.org/jrief/django-websocket-redis) [![Downloads](http://img.shields.io/pypi/dm/django-websocket-redis.svg?style=flat-square)](https://pypi.python.org/pypi/django-websocket-redis/) Questions --------- Please use the issue tracker to ask questions. License ------- Copyright © 2015 Jacob Rief. MIT licensed. Keywords: django,websocket,redis Platform: OS Independent Classifier: Environment :: Web Environment Classifier: Framework :: Django Classifier: Framework :: Django :: 1.5 Classifier: Framework :: Django :: 1.6 Classifier: Framework :: Django :: 1.7 Classifier: Framework :: Django :: 1.8 Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Development Status :: 4 - Beta Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 django-websocket-redis-0.4.7/django_websocket_redis.egg-info/requires.txt0000644000076500000240000000021413000363644027062 0ustar jriefstaff00000000000000setuptools redis gevent greenlet six [django-redis-sessions] django-redis-sessions>=0.4.0 [uwsgi] uWSGI>=1.9.20 [wsaccel] wsaccel>=0.6.2 django-websocket-redis-0.4.7/django_websocket_redis.egg-info/SOURCES.txt0000644000076500000240000000125013000363644026347 0ustar jriefstaff00000000000000LICENSE.txt MANIFEST.in README.md setup.py django_websocket_redis.egg-info/PKG-INFO django_websocket_redis.egg-info/SOURCES.txt django_websocket_redis.egg-info/dependency_links.txt django_websocket_redis.egg-info/not-zip-safe django_websocket_redis.egg-info/requires.txt django_websocket_redis.egg-info/top_level.txt ws4redis/__init__.py ws4redis/_compat.py ws4redis/context_processors.py ws4redis/django_runserver.py ws4redis/exceptions.py ws4redis/models.py ws4redis/publisher.py ws4redis/redis_store.py ws4redis/settings.py ws4redis/subscriber.py ws4redis/utf8validator.py ws4redis/uwsgi_runserver.py ws4redis/websocket.py ws4redis/wsgi_server.py ws4redis/static/js/ws4redis.jsdjango-websocket-redis-0.4.7/django_websocket_redis.egg-info/top_level.txt0000644000076500000240000000001113000363644027207 0ustar jriefstaff00000000000000ws4redis django-websocket-redis-0.4.7/LICENSE.txt0000644000076500000240000000203612636022307020123 0ustar jriefstaff00000000000000Copyright (c) 2013 Jacob Rief 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. django-websocket-redis-0.4.7/MANIFEST.in0000644000076500000240000000017312636022307020036 0ustar jriefstaff00000000000000include LICENSE.txt include README.md include setup.py recursive-include ws4redis *.py recursive-include ws4redis/static * django-websocket-redis-0.4.7/PKG-INFO0000644000076500000240000001111613000363644017372 0ustar jriefstaff00000000000000Metadata-Version: 1.1 Name: django-websocket-redis Version: 0.4.7 Summary: Websocket support for Django using Redis as datastore Home-page: https://github.com/jrief/django-websocket-redis Author: Jacob Rief Author-email: jacob.rief@gmail.com License: MIT Description: django-websocket-redis ====================== Project home: https://github.com/jrief/django-websocket-redis Detailed documentation on [ReadTheDocs](http://django-websocket-redis.readthedocs.org/en/latest/). Online demo: http://django-websocket-redis.awesto.com/ Websockets for Django using Redis as message queue -------------------------------------------------- This module implements websockets on top of Django without requiring any additional framework. For messaging it uses the [Redis](http://redis.io/) datastore and in a production environment, it is intended to work under [uWSGI](http://projects.unbit.it/uwsgi/) and behind [NGiNX](http://nginx.com/) or [Apache](http://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html) version 2.4.5 or later. New in 0.4.5 ------------ * Created 1 requirements file under ``examples/chatserver/requirements.txt``. * Renamed chatclient.py to test_chatclient.py - for django-nose testrunner. * Migrated example project to django 1.7. * Edited ``docs/testing.rst`` to show new changes for using example project. * Added support for Python 3.3 and 3.4. * Added support for Django-1.8 * Removes the check for middleware by name. Features -------- * Largely scalable for Django applications with many hundreds of open websocket connections. * Runs a seperate Django main loop in a cooperative concurrency model using [gevent](http://www.gevent.org/), thus only one thread/process is required to control *all* open websockets simultaneously. * Full control over this seperate main loop during development, so **Django** can be started as usual with ``./manage.py runserver``. * No dependency to any other asynchronous event driven framework, such as Tornado, Twisted or Socket.io/Node.js. * Normal Django requests communicate with this seperate main loop through **Redis** which, by the way is a good replacement for memcached. * Optionally persiting messages, allowing server reboots and client reconnections. If unsure, if this proposed architecture is the correct approach on how to integrate Websockets with Django, then please read Roberto De Ioris (BDFL of uWSGI) article about [Offloading Websockets and Server-Sent Events AKA “Combine them with Django safely”](http://uwsgi-docs.readthedocs.org/en/latest/articles/OffloadingWebsocketsAndSSE.html). Please also consider, that whichever alternative technology you use, you always need a message queue, so that the Django application can “talk” to the browser. This is because the only link between the browser and the server is through the Websocket and thus, by definition a long living connection. For scalability reasons you can't start a Django server thread for each of these connections. Build status ------------ [![Build Status](https://travis-ci.org/jrief/django-websocket-redis.png?branch=master)](https://travis-ci.org/jrief/django-websocket-redis) [![Downloads](http://img.shields.io/pypi/dm/django-websocket-redis.svg?style=flat-square)](https://pypi.python.org/pypi/django-websocket-redis/) Questions --------- Please use the issue tracker to ask questions. License ------- Copyright © 2015 Jacob Rief. MIT licensed. Keywords: django,websocket,redis Platform: OS Independent Classifier: Environment :: Web Environment Classifier: Framework :: Django Classifier: Framework :: Django :: 1.5 Classifier: Framework :: Django :: 1.6 Classifier: Framework :: Django :: 1.7 Classifier: Framework :: Django :: 1.8 Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Development Status :: 4 - Beta Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 django-websocket-redis-0.4.7/README.md0000644000076500000240000000613512646262561017574 0ustar jriefstaff00000000000000django-websocket-redis ====================== Project home: https://github.com/jrief/django-websocket-redis Detailed documentation on [ReadTheDocs](http://django-websocket-redis.readthedocs.org/en/latest/). Online demo: http://django-websocket-redis.awesto.com/ Websockets for Django using Redis as message queue -------------------------------------------------- This module implements websockets on top of Django without requiring any additional framework. For messaging it uses the [Redis](http://redis.io/) datastore and in a production environment, it is intended to work under [uWSGI](http://projects.unbit.it/uwsgi/) and behind [NGiNX](http://nginx.com/) or [Apache](http://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html) version 2.4.5 or later. New in 0.4.5 ------------ * Created 1 requirements file under ``examples/chatserver/requirements.txt``. * Renamed chatclient.py to test_chatclient.py - for django-nose testrunner. * Migrated example project to django 1.7. * Edited ``docs/testing.rst`` to show new changes for using example project. * Added support for Python 3.3 and 3.4. * Added support for Django-1.8 * Removes the check for middleware by name. Features -------- * Largely scalable for Django applications with many hundreds of open websocket connections. * Runs a seperate Django main loop in a cooperative concurrency model using [gevent](http://www.gevent.org/), thus only one thread/process is required to control *all* open websockets simultaneously. * Full control over this seperate main loop during development, so **Django** can be started as usual with ``./manage.py runserver``. * No dependency to any other asynchronous event driven framework, such as Tornado, Twisted or Socket.io/Node.js. * Normal Django requests communicate with this seperate main loop through **Redis** which, by the way is a good replacement for memcached. * Optionally persiting messages, allowing server reboots and client reconnections. If unsure, if this proposed architecture is the correct approach on how to integrate Websockets with Django, then please read Roberto De Ioris (BDFL of uWSGI) article about [Offloading Websockets and Server-Sent Events AKA “Combine them with Django safely”](http://uwsgi-docs.readthedocs.org/en/latest/articles/OffloadingWebsocketsAndSSE.html). Please also consider, that whichever alternative technology you use, you always need a message queue, so that the Django application can “talk” to the browser. This is because the only link between the browser and the server is through the Websocket and thus, by definition a long living connection. For scalability reasons you can't start a Django server thread for each of these connections. Build status ------------ [![Build Status](https://travis-ci.org/jrief/django-websocket-redis.png?branch=master)](https://travis-ci.org/jrief/django-websocket-redis) [![Downloads](http://img.shields.io/pypi/dm/django-websocket-redis.svg?style=flat-square)](https://pypi.python.org/pypi/django-websocket-redis/) Questions --------- Please use the issue tracker to ask questions. License ------- Copyright © 2015 Jacob Rief. MIT licensed. django-websocket-redis-0.4.7/setup.cfg0000644000076500000240000000007313000363644020116 0ustar jriefstaff00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 django-websocket-redis-0.4.7/setup.py0000644000076500000240000000347712636025533020030 0ustar jriefstaff00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import unicode_literals from setuptools import setup, find_packages from ws4redis import __version__ try: from pypandoc import convert except ImportError: import io def convert(filename, fmt): with io.open(filename, encoding='utf-8') as fd: return fd.read() DESCRIPTION = 'Websocket support for Django using Redis as datastore' CLASSIFIERS = [ 'Environment :: Web Environment', 'Framework :: Django', 'Framework :: Django :: 1.5', 'Framework :: Django :: 1.6', 'Framework :: Django :: 1.7', 'Framework :: Django :: 1.8', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Software Development :: Libraries :: Python Modules', 'Development Status :: 4 - Beta', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', ] setup( name='django-websocket-redis', version=__version__, author='Jacob Rief', author_email='jacob.rief@gmail.com', description=DESCRIPTION, long_description=convert('README.md', 'rst'), url='https://github.com/jrief/django-websocket-redis', license='MIT', keywords=['django', 'websocket', 'redis'], platforms=['OS Independent'], classifiers=CLASSIFIERS, packages=find_packages(exclude=['examples', 'docs']), include_package_data=True, install_requires=[ 'setuptools', 'redis', 'gevent', 'greenlet', 'six', ], extras_require={ 'uwsgi': ['uWSGI>=1.9.20'], 'wsaccel': ['wsaccel>=0.6.2'], 'django-redis-sessions': ['django-redis-sessions>=0.4.0'], }, zip_safe=False, ) django-websocket-redis-0.4.7/ws4redis/0000755000076500000240000000000013000363644020041 5ustar jriefstaff00000000000000django-websocket-redis-0.4.7/ws4redis/__init__.py0000644000076500000240000000005613000361167022151 0ustar jriefstaff00000000000000# -*- coding: utf-8 -*- __version__ = '0.4.7' django-websocket-redis-0.4.7/ws4redis/_compat.py0000644000076500000240000000107013000361143022023 0ustar jriefstaff00000000000000def is_authenticated(request): """Wrapper for checking when a request is authenticated. This checks first for a valid request and user, then checks to see if `is_authenticated` is a callable in order to be compatible with Django 1.10, wherein using a callable for `is_authenticated` is deprecated in favor of a property. """ if not request: return False if not request.user: return False if callable(request.user.is_authenticated): return request.user.is_authenticated() return request.user.is_authenticated django-websocket-redis-0.4.7/ws4redis/context_processors.py0000644000076500000240000000104112636022307024357 0ustar jriefstaff00000000000000# -*- coding: utf-8 -*- from django.utils.safestring import mark_safe from ws4redis import settings def default(request): """ Adds additional context variables to the default context. """ protocol = request.is_secure() and 'wss://' or 'ws://' heartbeat_msg = settings.WS4REDIS_HEARTBEAT and '"{0}"'.format(settings.WS4REDIS_HEARTBEAT) or 'null' context = { 'WEBSOCKET_URI': protocol + request.get_host() + settings.WEBSOCKET_URL, 'WS4REDIS_HEARTBEAT': mark_safe(heartbeat_msg), } return context django-websocket-redis-0.4.7/ws4redis/django_runserver.py0000644000076500000240000000673212646262561024013 0ustar jriefstaff00000000000000#-*- coding: utf-8 -*- import six import base64 import select from hashlib import sha1 from wsgiref import util from django.core.wsgi import get_wsgi_application from django.core.servers.basehttp import WSGIServer, WSGIRequestHandler from django.core.handlers.wsgi import logger from django.conf import settings from django.core.management.commands import runserver from django.utils.six.moves import socketserver from django.utils.encoding import force_str from ws4redis.websocket import WebSocket from ws4redis.wsgi_server import WebsocketWSGIServer, HandshakeError, UpgradeRequiredError util._hoppish = {}.__contains__ class WebsocketRunServer(WebsocketWSGIServer): WS_GUID = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11' WS_VERSIONS = ('13', '8', '7') def upgrade_websocket(self, environ, start_response): """ Attempt to upgrade the socket environ['wsgi.input'] into a websocket enabled connection. """ websocket_version = environ.get('HTTP_SEC_WEBSOCKET_VERSION', '') if not websocket_version: raise UpgradeRequiredError elif websocket_version not in self.WS_VERSIONS: raise HandshakeError('Unsupported WebSocket Version: {0}'.format(websocket_version)) key = environ.get('HTTP_SEC_WEBSOCKET_KEY', '').strip() if not key: raise HandshakeError('Sec-WebSocket-Key header is missing/empty') try: key_len = len(base64.b64decode(key)) except TypeError: raise HandshakeError('Invalid key: {0}'.format(key)) if key_len != 16: # 5.2.1 (3) raise HandshakeError('Invalid key: {0}'.format(key)) sec_ws_accept = base64.b64encode(sha1(six.b(key) + self.WS_GUID).digest()) if six.PY3: sec_ws_accept = sec_ws_accept.decode('ascii') headers = [ ('Upgrade', 'websocket'), ('Connection', 'Upgrade'), ('Sec-WebSocket-Accept', sec_ws_accept), ('Sec-WebSocket-Version', str(websocket_version)) ] if environ.get('HTTP_SEC_WEBSOCKET_PROTOCOL') is not None: headers.append(('Sec-WebSocket-Protocol', environ.get('HTTP_SEC_WEBSOCKET_PROTOCOL'))) logger.debug('WebSocket request accepted, switching protocols') start_response(force_str('101 Switching Protocols'), headers) six.get_method_self(start_response).finish_content() return WebSocket(environ['wsgi.input']) def select(self, rlist, wlist, xlist, timeout=None): return select.select(rlist, wlist, xlist, timeout) def run(addr, port, wsgi_handler, ipv6=False, threading=False): """ Function to monkey patch the internal Django command: manage.py runserver """ logger.info('Websocket support is enabled') server_address = (addr, port) if not threading: raise Exception("Django's Websocket server must run with threading enabled") httpd_cls = type('WSGIServer', (socketserver.ThreadingMixIn, WSGIServer), {'daemon_threads': True}) httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6) httpd.set_app(wsgi_handler) httpd.serve_forever() runserver.run = run _django_app = get_wsgi_application() _websocket_app = WebsocketRunServer() _websocket_url = getattr(settings, 'WEBSOCKET_URL') def application(environ, start_response): if _websocket_url and environ.get('PATH_INFO').startswith(_websocket_url): return _websocket_app(environ, start_response) return _django_app(environ, start_response) django-websocket-redis-0.4.7/ws4redis/exceptions.py0000644000076500000240000000103412636022307022574 0ustar jriefstaff00000000000000#-*- coding: utf-8 -*- from socket import error as socket_error from django.http import BadHeaderError class WebSocketError(socket_error): """ Raised when an active websocket encounters a problem. """ class FrameTooLargeException(WebSocketError): """ Raised if a received frame is too large. """ class HandshakeError(BadHeaderError): """ Raised if an error occurs during protocol handshake. """ class UpgradeRequiredError(HandshakeError): """ Raised if protocol must be upgraded. """ django-websocket-redis-0.4.7/ws4redis/models.py0000644000076500000240000000110612636022307021676 0ustar jriefstaff00000000000000# -*- coding: utf-8 -*- from django.contrib.auth.signals import user_logged_in from django.dispatch import receiver @receiver(user_logged_in) def store_groups_in_session(sender, user, request, **kwargs): """ When a user logs in, fetch its groups and store them in the users session. This is required by ws4redis, since fetching groups accesses the database, which is a blocking operation and thus not allowed from within the websocket loop. """ if hasattr(user, 'groups'): request.session['ws4redis:memberof'] = [g.name for g in user.groups.all()] django-websocket-redis-0.4.7/ws4redis/publisher.py0000644000076500000240000000463613000361143022411 0ustar jriefstaff00000000000000#-*- coding: utf-8 -*- from redis import ConnectionPool, StrictRedis from ws4redis import settings from ws4redis.redis_store import RedisStore from ws4redis._compat import is_authenticated redis_connection_pool = ConnectionPool(**settings.WS4REDIS_CONNECTION) class RedisPublisher(RedisStore): def __init__(self, **kwargs): """ Initialize the channels for publishing messages through the message queue. """ connection = StrictRedis(connection_pool=redis_connection_pool) super(RedisPublisher, self).__init__(connection) for key in self._get_message_channels(**kwargs): self._publishers.add(key) def fetch_message(self, request, facility, audience='any'): """ Fetch the first message available for the given ``facility`` and ``audience``, if it has been persisted in the Redis datastore. The current HTTP ``request`` is used to determine to whom the message belongs. A unique string is used to identify the bucket's ``facility``. Determines the ``audience`` to check for the message. Must be one of ``broadcast``, ``group``, ``user``, ``session`` or ``any``. The default is ``any``, which means to check for all possible audiences. """ prefix = self.get_prefix() channels = [] if audience in ('session', 'any',): if request and request.session: channels.append('{prefix}session:{0}:{facility}'.format(request.session.session_key, prefix=prefix, facility=facility)) if audience in ('user', 'any',): if is_authenticated(request): channels.append('{prefix}user:{0}:{facility}'.format(request.user.get_username(), prefix=prefix, facility=facility)) if audience in ('group', 'any',): try: if is_authenticated(request): groups = request.session['ws4redis:memberof'] channels.extend('{prefix}group:{0}:{facility}'.format(g, prefix=prefix, facility=facility) for g in groups) except (KeyError, AttributeError): pass if audience in ('broadcast', 'any',): channels.append('{prefix}broadcast:{facility}'.format(prefix=prefix, facility=facility)) for channel in channels: message = self._connection.get(channel) if message: return message django-websocket-redis-0.4.7/ws4redis/redis_store.py0000644000076500000240000001765613000361143022744 0ustar jriefstaff00000000000000# -*- coding: utf-8 -*- import six import warnings from ws4redis import settings from ws4redis._compat import is_authenticated """ A type instance to handle the special case, when a request shall refer to itself, or as a user, or as a group. """ SELF = type('SELF_TYPE', (object,), {})() def _wrap_users(users, request): """ Returns a list with the given list of users and/or the currently logged in user, if the list contains the magic item SELF. """ result = set() for u in users: if u is SELF and is_authenticated(request): result.add(request.user.get_username()) else: result.add(u) return result def _wrap_groups(groups, request): """ Returns a list of groups for the given list of groups and/or the current logged in user, if the list contains the magic item SELF. Note that this method bypasses Django's own group resolution, which requires a database query and thus is unsuitable for coroutines. Therefore the membership is takes from the session store, which is filled by a signal handler, while the users logs in. """ result = set() for g in groups: if g is SELF and is_authenticated(request): result.update(request.session.get('ws4redis:memberof', [])) else: result.add(g) return result def _wrap_sessions(sessions, request): """ Returns a list of session keys for the given lists of sessions and/or the session key of the current logged in user, if the list contains the magic item SELF. """ result = set() for s in sessions: if s is SELF and request: result.add(request.session.session_key) else: result.add(s) return result class RedisMessage(six.binary_type): """ A class wrapping messages to be send and received through RedisStore. This class behaves like a normal string class, but silently discards heartbeats and converts messages received from Redis. """ def __new__(cls, value): if six.PY3: if isinstance(value, str): if value != settings.WS4REDIS_HEARTBEAT: value = value.encode() return super(RedisMessage, cls).__new__(cls, value) elif isinstance(value, bytes): if settings.WS4REDIS_HEARTBEAT is None or value != settings.WS4REDIS_HEARTBEAT.encode(): return super(RedisMessage, cls).__new__(cls, value) elif isinstance(value, list): if len(value) >= 2 and value[0] == b'message': return super(RedisMessage, cls).__new__(cls, value[2]) else: if isinstance(value, six.string_types): if value != settings.WS4REDIS_HEARTBEAT: return six.binary_type.__new__(cls, value) elif isinstance(value, list): if len(value) >= 2 and value[0] == 'message': return six.binary_type.__new__(cls, value[2]) return None class RedisStore(object): """ Abstract base class to control publishing and subscription for messages to and from the Redis datastore. """ _expire = settings.WS4REDIS_EXPIRE def __init__(self, connection): self._connection = connection self._publishers = set() def publish_message(self, message, expire=None): """ Publish a ``message`` on the subscribed channel on the Redis datastore. ``expire`` sets the time in seconds, on how long the message shall additionally of being published, also be persisted in the Redis datastore. If unset, it defaults to the configuration settings ``WS4REDIS_EXPIRE``. """ if expire is None: expire = self._expire if not isinstance(message, RedisMessage): raise ValueError('message object is not of type RedisMessage') for channel in self._publishers: self._connection.publish(channel, message) if expire > 0: self._connection.setex(channel, expire, message) @staticmethod def get_prefix(): return settings.WS4REDIS_PREFIX and '{0}:'.format(settings.WS4REDIS_PREFIX) or '' def _get_message_channels(self, request=None, facility='{facility}', broadcast=False, groups=(), users=(), sessions=()): prefix = self.get_prefix() channels = [] if broadcast is True: # broadcast message to each subscriber listening on the named facility channels.append('{prefix}broadcast:{facility}'.format(prefix=prefix, facility=facility)) # handle group messaging if isinstance(groups, (list, tuple)): # message is delivered to all listed groups channels.extend('{prefix}group:{0}:{facility}'.format(g, prefix=prefix, facility=facility) for g in _wrap_groups(groups, request)) elif groups is True and is_authenticated(request): # message is delivered to all groups the currently logged in user belongs to warnings.warn('Wrap groups=True into a list or tuple using SELF', DeprecationWarning) channels.extend('{prefix}group:{0}:{facility}'.format(g, prefix=prefix, facility=facility) for g in request.session.get('ws4redis:memberof', [])) elif isinstance(groups, basestring): # message is delivered to the named group warnings.warn('Wrap a single group into a list or tuple', DeprecationWarning) channels.append('{prefix}group:{0}:{facility}'.format(groups, prefix=prefix, facility=facility)) elif not isinstance(groups, bool): raise ValueError('Argument `groups` must be a list or tuple') # handle user messaging if isinstance(users, (list, tuple)): # message is delivered to all listed users channels.extend('{prefix}user:{0}:{facility}'.format(u, prefix=prefix, facility=facility) for u in _wrap_users(users, request)) elif users is True and is_authenticated(request): # message is delivered to browser instances of the currently logged in user warnings.warn('Wrap users=True into a list or tuple using SELF', DeprecationWarning) channels.append('{prefix}user:{0}:{facility}'.format(request.user.get_username(), prefix=prefix, facility=facility)) elif isinstance(users, basestring): # message is delivered to the named user warnings.warn('Wrap a single user into a list or tuple', DeprecationWarning) channels.append('{prefix}user:{0}:{facility}'.format(users, prefix=prefix, facility=facility)) elif not isinstance(users, bool): raise ValueError('Argument `users` must be a list or tuple') # handle session messaging if isinstance(sessions, (list, tuple)): # message is delivered to all browsers instances listed in sessions channels.extend('{prefix}session:{0}:{facility}'.format(s, prefix=prefix, facility=facility) for s in _wrap_sessions(sessions, request)) elif sessions is True and request and request.session: # message is delivered to browser instances owning the current session warnings.warn('Wrap a single session key into a list or tuple using SELF', DeprecationWarning) channels.append('{prefix}session:{0}:{facility}'.format(request.session.session_key, prefix=prefix, facility=facility)) elif isinstance(sessions, basestring): # message is delivered to the named user warnings.warn('Wrap a single session key into a list or tuple', DeprecationWarning) channels.append('{prefix}session:{0}:{facility}'.format(sessions, prefix=prefix, facility=facility)) elif not isinstance(sessions, bool): raise ValueError('Argument `sessions` must be a list or tuple') return channels django-websocket-redis-0.4.7/ws4redis/settings.py0000644000076500000240000000376512636022307022270 0ustar jriefstaff00000000000000# -*- coding: utf-8 -*- from django.conf import settings WEBSOCKET_URL = getattr(settings, 'WEBSOCKET_URL', '/ws/') WS4REDIS_CONNECTION = getattr(settings, 'WS4REDIS_CONNECTION', { 'host': 'localhost', 'port': 6379, 'db': 0, 'password': None, }) """ A string to prefix elements in the Redis datastore, to avoid naming conflicts with other services. """ WS4REDIS_PREFIX = getattr(settings, 'WS4REDIS_PREFIX', None) """ The time in seconds, items shall be persisted by the Redis datastore. """ WS4REDIS_EXPIRE = getattr(settings, 'WS4REDIS_EXPIRE', 3600) """ Replace the subscriber class by a customized version. """ WS4REDIS_SUBSCRIBER = getattr(settings, 'WS4REDIS_SUBSCRIBER', 'ws4redis.subscriber.RedisSubscriber') """ This set the magic string to recognize heartbeat messages. If set, this message string is ignored by the server and also shall be ignored on the client. If WS4REDIS_HEARTBEAT is not None, the server sends at least every 4 seconds a heartbeat message. It is then up to the client to decide, what to do with these messages. """ WS4REDIS_HEARTBEAT = getattr(settings, 'WS4REDIS_HEARTBEAT', None) """ If set, this callback function is called right after the initialization of the Websocket. This function can be used to restrict the subscription/publishing channels for the current client. As its first parameter, it takes the current ``request`` object. The second parameter is a list of desired subscription channels. This callback function shall return a list of allowed channels or throw a ``PermissionDenied`` exception. Remember that this function is not allowed to perform any blocking requests, such as accessing the database! """ WS4REDIS_ALLOWED_CHANNELS = getattr(settings, 'WS4REDIS_ALLOWED_CHANNELS', None) """ If set, this callback function is called instead of the default process_request function in WebsocketWSGIServer. This function can be used to enforce custom authentication flow. i.e. JWT """ WS4REDIS_PROCESS_REQUEST = getattr(settings, 'WS4REDIS_PROCESS_REQUEST', None) django-websocket-redis-0.4.7/ws4redis/static/0000755000076500000240000000000013000363644021330 5ustar jriefstaff00000000000000django-websocket-redis-0.4.7/ws4redis/static/js/0000755000076500000240000000000013000363644021744 5ustar jriefstaff00000000000000django-websocket-redis-0.4.7/ws4redis/static/js/ws4redis.js0000644000076500000240000001044213000361143024037 0ustar jriefstaff00000000000000/** * options.uri - > The Websocket URI * options.connected -> Callback called after the websocket is connected. * options.connecting -> Callback called when the websocket is connecting. * options.disconnected -> Callback called after the websocket is disconnected. * options.receive_message -> Callback called when a message is received from the websocket. * options.heartbeat_msg -> String to identify the heartbeat message. * $ -> JQuery instance. */ function WS4Redis(options, $) { 'use strict'; var opts, ws, deferred, timer, attempts = 1, must_reconnect = true; var heartbeat_interval = null, missed_heartbeats = 0; if (this === undefined) return new WS4Redis(options, $); if (options.uri === undefined) throw new Error('No Websocket URI in options'); if ($ === undefined) $ = jQuery; opts = $.extend({ heartbeat_msg: null }, options); connect(opts.uri); function connect(uri) { try { if (ws && (is_connecting() || is_connected())) { console.log("Websocket is connecting or already connected."); return; } if ($.type(opts.connecting) === 'function') { opts.connecting(); } console.log("Connecting to " + uri + " ..."); deferred = $.Deferred(); ws = new WebSocket(uri); ws.onopen = on_open; ws.onmessage = on_message; ws.onerror = on_error; ws.onclose = on_close; timer = null; } catch (err) { try_to_reconnect(); deferred.reject(new Error(err)); } } function try_to_reconnect() { if (must_reconnect && !timer) { // try to reconnect console.log('Reconnecting...'); var interval = generate_inteval(attempts); timer = setTimeout(function() { attempts++; connect(ws.url); }, interval); } } function send_heartbeat() { try { missed_heartbeats++; if (missed_heartbeats > 3) throw new Error("Too many missed heartbeats."); ws.send(opts.heartbeat_msg); } catch(e) { clearInterval(heartbeat_interval); heartbeat_interval = null; console.warn("Closing connection. Reason: " + e.message); if ( !is_closing() && !is_closed() ) { ws.close(); } } } function on_open() { console.log('Connected!'); // new connection, reset attemps counter attempts = 1; deferred.resolve(); if (opts.heartbeat_msg && heartbeat_interval === null) { missed_heartbeats = 0; heartbeat_interval = setInterval(send_heartbeat, 5000); } if ($.type(opts.connected) === 'function') { opts.connected(); } } function on_close(evt) { console.log("Connection closed!"); if ($.type(opts.disconnected) === 'function') { opts.disconnected(evt); } try_to_reconnect(); } function on_error(evt) { console.error("Websocket connection is broken!"); deferred.reject(new Error(evt)); } function on_message(evt) { if (opts.heartbeat_msg && evt.data === opts.heartbeat_msg) { // reset the counter for missed heartbeats missed_heartbeats = 0; } else if ($.type(opts.receive_message) === 'function') { return opts.receive_message(evt.data); } } // this code is borrowed from http://blog.johnryding.com/post/78544969349/ // // Generate an interval that is randomly between 0 and 2^k - 1, where k is // the number of connection attmpts, with a maximum interval of 30 seconds, // so it starts at 0 - 1 seconds and maxes out at 0 - 30 seconds function generate_inteval(k) { var maxInterval = (Math.pow(2, k) - 1) * 1000; // If the generated interval is more than 30 seconds, truncate it down to 30 seconds. if (maxInterval > 30*1000) { maxInterval = 30*1000; } // generate the interval to a random number between 0 and the maxInterval determined from above return Math.random() * maxInterval; } this.send_message = function(message) { ws.send(message); }; this.get_state = function() { return ws.readyState; }; function is_connecting() { return ws && ws.readyState === 0; } function is_connected() { return ws && ws.readyState === 1; } function is_closing() { return ws && ws.readyState === 2; } function is_closed() { return ws && ws.readyState === 3; } this.close = function () { clearInterval(heartbeat_interval); must_reconnect = false; if (!is_closing() || !is_closed()) { ws.close(); } } this.is_connecting = is_connecting; this.is_connected = is_connected; this.is_closing = is_closing; this.is_closed = is_closed; } django-websocket-redis-0.4.7/ws4redis/subscriber.py0000644000076500000240000000607212636022307022565 0ustar jriefstaff00000000000000# -*- coding: utf-8 -*- from django.conf import settings from ws4redis.redis_store import RedisStore, SELF class RedisSubscriber(RedisStore): """ Subscriber class, used by the websocket code to listen for subscribed channels """ subscription_channels = ['subscribe-session', 'subscribe-group', 'subscribe-user', 'subscribe-broadcast'] publish_channels = ['publish-session', 'publish-group', 'publish-user', 'publish-broadcast'] def __init__(self, connection): self._subscription = None super(RedisSubscriber, self).__init__(connection) def parse_response(self): """ Parse a message response sent by the Redis datastore on a subscribed channel. """ return self._subscription.parse_response() def set_pubsub_channels(self, request, channels): """ Initialize the channels used for publishing and subscribing messages through the message queue. """ facility = request.path_info.replace(settings.WEBSOCKET_URL, '', 1) # initialize publishers audience = { 'users': 'publish-user' in channels and [SELF] or [], 'groups': 'publish-group' in channels and [SELF] or [], 'sessions': 'publish-session' in channels and [SELF] or [], 'broadcast': 'publish-broadcast' in channels, } self._publishers = set() for key in self._get_message_channels(request=request, facility=facility, **audience): self._publishers.add(key) # initialize subscribers audience = { 'users': 'subscribe-user' in channels and [SELF] or [], 'groups': 'subscribe-group' in channels and [SELF] or [], 'sessions': 'subscribe-session' in channels and [SELF] or [], 'broadcast': 'subscribe-broadcast' in channels, } self._subscription = self._connection.pubsub() for key in self._get_message_channels(request=request, facility=facility, **audience): self._subscription.subscribe(key) def send_persited_messages(self, websocket): """ This method is called immediately after a websocket is openend by the client, so that persisted messages can be sent back to the client upon connection. """ for channel in self._subscription.channels: message = self._connection.get(channel) if message: websocket.send(message) def get_file_descriptor(self): """ Returns the file descriptor used for passing to the select call when listening on the message queue. """ return self._subscription.connection and self._subscription.connection._sock.fileno() def release(self): """ New implementation to free up Redis subscriptions when websockets close. This prevents memory sap when Redis Output Buffer and Output Lists build when websockets are abandoned. """ if self._subscription and self._subscription.subscribed: self._subscription.unsubscribe() self._subscription.reset() django-websocket-redis-0.4.7/ws4redis/utf8validator.py0000644000076500000240000001233312636022307023213 0ustar jriefstaff00000000000000############################################################################### ## ## Copyright 2011-2013 Tavendo GmbH ## ## Note: ## ## This code is a Python implementation of the algorithm ## ## "Flexible and Economical UTF-8 Decoder" ## ## by Bjoern Hoehrmann ## ## bjoern@hoehrmann.de ## http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ ## ## Licensed under the Apache License, Version 2.0 (the "License"); ## you may not use this file except in compliance with the License. ## You may obtain a copy of the License at ## ## http://www.apache.org/licenses/LICENSE-2.0 ## ## Unless required by applicable law or agreed to in writing, software ## distributed under the License is distributed on an "AS IS" BASIS, ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ## See the License for the specific language governing permissions and ## limitations under the License. ## ############################################################################### import six if six.PY3: xrange = range ## use Cython implementation of UTF8 validator if available ## try: from wsaccel.utf8validator import Utf8Validator except: ## fallback to pure Python implementation class Utf8Validator: """ Incremental UTF-8 validator with constant memory consumption (minimal state). Implements the algorithm "Flexible and Economical UTF-8 Decoder" by Bjoern Hoehrmann (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/). """ ## DFA transitions UTF8VALIDATOR_DFA = [ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 00..1f 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 20..3f 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 40..5f 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 60..7f 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, # 80..9f 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, # a0..bf 8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, # c0..df 0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, # e0..ef 0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, # f0..ff 0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, # s0..s0 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, # s1..s2 1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, # s3..s4 1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, # s5..s6 1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, # s7..s8 ] UTF8_ACCEPT = 0 UTF8_REJECT = 1 def __init__(self): self.state = None self.codepoint = None self.i = None self.reset() def decode(self, b): """ Eat one UTF-8 octet, and validate on the fly. Returns UTF8_ACCEPT when enough octets have been consumed, in which case self.codepoint contains the decoded Unicode code point. Returns UTF8_REJECT when invalid UTF-8 was encountered. Returns some other positive integer when more octets need to be eaten. """ type = Utf8Validator.UTF8VALIDATOR_DFA[b] if self.state != Utf8Validator.UTF8_ACCEPT: self.codepoint = (b & 0x3f) | (self.codepoint << 6) else: self.codepoint = (0xff >> type) & b self.state = Utf8Validator.UTF8VALIDATOR_DFA[256 + self.state * 16 + type] return self.state def reset(self): """ Reset validator to start new incremental UTF-8 decode/validation. """ self.state = Utf8Validator.UTF8_ACCEPT self.codepoint = 0 self.i = 0 def validate(self, ba): """ Incrementally validate a chunk of bytes provided as string. Will return a quad (valid?, endsOnCodePoint?, currentIndex, totalIndex). As soon as an octet is encountered which renders the octet sequence invalid, a quad with valid? == False is returned. currentIndex returns the index within the currently consumed chunk, and totalIndex the index within the total consumed sequence that was the point of bail out. When valid? == True, currentIndex will be len(ba) and totalIndex the total amount of consumed bytes. """ if not isinstance(ba, six.text_type): ba = ba.decode() l = len(ba) for i in xrange(l): ## optimized version of decode(), since we are not interested in actual code points self.state = Utf8Validator.UTF8VALIDATOR_DFA[256 + (self.state << 4) + Utf8Validator.UTF8VALIDATOR_DFA[ord(ba[i])]] if self.state == Utf8Validator.UTF8_REJECT: self.i += i return False, False, i, self.i self.i += l return True, self.state == Utf8Validator.UTF8_ACCEPT, l, self.i django-websocket-redis-0.4.7/ws4redis/uwsgi_runserver.py0000644000076500000240000000303412636022307023666 0ustar jriefstaff00000000000000# -*- coding: utf-8 -*- import uwsgi import gevent.select from ws4redis.exceptions import WebSocketError from ws4redis.wsgi_server import WebsocketWSGIServer class uWSGIWebsocket(object): def __init__(self): self._closed = False def get_file_descriptor(self): """Return the file descriptor for the given websocket""" try: return uwsgi.connection_fd() except IOError as e: self.close() raise WebSocketError(e) @property def closed(self): return self._closed def receive(self): if self._closed: raise WebSocketError("Connection is already closed") try: return uwsgi.websocket_recv_nb() except IOError as e: self.close() raise WebSocketError(e) def flush(self): try: uwsgi.websocket_recv_nb() except IOError: self.close() def send(self, message, binary=None): try: uwsgi.websocket_send(message) except IOError as e: self.close() raise WebSocketError(e) def close(self, code=1000, message=''): self._closed = True class uWSGIWebsocketServer(WebsocketWSGIServer): def upgrade_websocket(self, environ, start_response): uwsgi.websocket_handshake(environ['HTTP_SEC_WEBSOCKET_KEY'], environ.get('HTTP_ORIGIN', '')) return uWSGIWebsocket() def select(self, rlist, wlist, xlist, timeout=None): return gevent.select.select(rlist, wlist, xlist, timeout) django-websocket-redis-0.4.7/ws4redis/websocket.py0000644000076500000240000003453412646262561022425 0ustar jriefstaff00000000000000# -*- coding: utf-8 -*- # This code was generously pilfered from https://bitbucket.org/Jeffrey/gevent-websocket # written by Jeffrey Gelens (http://noppo.pro/) and licensed under the Apache License, Version 2.0 import six import struct from socket import error as socket_error from django.core.handlers.wsgi import logger from ws4redis.utf8validator import Utf8Validator from ws4redis.exceptions import WebSocketError, FrameTooLargeException if six.PY3: xrange = range class WebSocket(object): __slots__ = ('_closed', 'stream', 'utf8validator', 'utf8validate_last') OPCODE_CONTINUATION = 0x00 OPCODE_TEXT = 0x01 OPCODE_BINARY = 0x02 OPCODE_CLOSE = 0x08 OPCODE_PING = 0x09 OPCODE_PONG = 0x0a def __init__(self, wsgi_input): self._closed = False self.stream = Stream(wsgi_input) self.utf8validator = Utf8Validator() self.utf8validate_last = None def __del__(self): try: self.close() except: # close() may fail if __init__ didn't complete pass def _decode_bytes(self, bytestring): """ Internal method used to convert the utf-8 encoded bytestring into unicode. If the conversion fails, the socket will be closed. """ if not bytestring: return u'' try: return bytestring.decode('utf-8') except UnicodeDecodeError: self.close(1007) raise def _encode_bytes(self, text): """ :returns: The utf-8 byte string equivalent of `text`. """ if isinstance(text, six.binary_type): return text if not isinstance(text, six.text_type): text = six.text_type(text or '') return text.encode('utf-8') def _is_valid_close_code(self, code): """ :returns: Whether the returned close code is a valid hybi return code. """ if code < 1000: return False if 1004 <= code <= 1006: return False if 1012 <= code <= 1016: return False if code == 1100: # not sure about this one but the autobahn fuzzer requires it. return False if 2000 <= code <= 2999: return False return True def get_file_descriptor(self): """Return the file descriptor for the given websocket""" return self.stream.fileno @property def closed(self): return self._closed def handle_close(self, header, payload): """ Called when a close frame has been decoded from the stream. :param header: The decoded `Header`. :param payload: The bytestring payload associated with the close frame. """ if not payload: self.close(1000, None) return if len(payload) < 2: raise WebSocketError('Invalid close frame: {0} {1}'.format(header, payload)) rv = payload[:2] if six.PY2: code = struct.unpack('!H', str(rv))[0] else: code = struct.unpack('!H', bytes(rv))[0] payload = payload[2:] if payload: validator = Utf8Validator() val = validator.validate(payload) if not val[0]: raise UnicodeError if not self._is_valid_close_code(code): raise WebSocketError('Invalid close code {0}'.format(code)) self.close(code, payload) def handle_ping(self, header, payload): self.send_frame(payload, self.OPCODE_PONG) def handle_pong(self, header, payload): pass def read_frame(self): """ Block until a full frame has been read from the socket. This is an internal method as calling this will not cleanup correctly if an exception is called. Use `receive` instead. :return: The header and payload as a tuple. """ header = Header.decode_header(self.stream) if header.flags: raise WebSocketError if not header.length: return header, '' try: payload = self.stream.read(header.length) except socket_error: payload = '' except Exception: logger.debug("{}: {}".format(type(e), six.text_type(e))) payload = '' if len(payload) != header.length: raise WebSocketError('Unexpected EOF reading frame payload') if header.mask: payload = header.unmask_payload(payload) return header, payload def validate_utf8(self, payload): # Make sure the frames are decodable independently self.utf8validate_last = self.utf8validator.validate(payload) if not self.utf8validate_last[0]: raise UnicodeError("Encountered invalid UTF-8 while processing " "text message at payload octet index " "{0:d}".format(self.utf8validate_last[3])) def read_message(self): """ Return the next text or binary message from the socket. This is an internal method as calling this will not cleanup correctly if an exception is called. Use `receive` instead. """ opcode = None message = "" while True: header, payload = self.read_frame() f_opcode = header.opcode if f_opcode in (self.OPCODE_TEXT, self.OPCODE_BINARY): # a new frame if opcode: raise WebSocketError("The opcode in non-fin frame is expected to be zero, got {0!r}".format(f_opcode)) # Start reading a new message, reset the validator self.utf8validator.reset() self.utf8validate_last = (True, True, 0, 0) opcode = f_opcode elif f_opcode == self.OPCODE_CONTINUATION: if not opcode: raise WebSocketError("Unexpected frame with opcode=0") elif f_opcode == self.OPCODE_PING: self.handle_ping(header, payload) continue elif f_opcode == self.OPCODE_PONG: self.handle_pong(header, payload) continue elif f_opcode == self.OPCODE_CLOSE: self.handle_close(header, payload) return else: raise WebSocketError("Unexpected opcode={0!r}".format(f_opcode)) if opcode == self.OPCODE_TEXT: self.validate_utf8(payload) if six.PY3: payload = payload.decode() message += payload if header.fin: break if opcode == self.OPCODE_TEXT: if six.PY2: self.validate_utf8(message) else: self.validate_utf8(message.encode()) return message else: return bytearray(message) def receive(self): """ Read and return a message from the stream. If `None` is returned, then the socket is considered closed/errored. """ if self._closed: raise WebSocketError("Connection is already closed") try: return self.read_message() except UnicodeError as e: logger.info('websocket.receive: UnicodeError {}'.format(e)) self.close(1007) except WebSocketError as e: logger.info('websocket.receive: WebSocketError {}'.format(e)) self.close(1002) except Exception as e: logger.info('websocket.receive: Unknown error {}'.format(e)) raise e def flush(self): """ Flush a websocket. In this implementation intentionally it does nothing. """ pass def send_frame(self, message, opcode): """ Send a frame over the websocket with message as its payload """ if self._closed: raise WebSocketError("Connection is already closed") if opcode == self.OPCODE_TEXT: message = self._encode_bytes(message) elif opcode == self.OPCODE_BINARY: message = six.binary_type(message) header = Header.encode_header(True, opcode, '', len(message), 0) try: self.stream.write(header + message) except socket_error: raise WebSocketError("Socket is dead") def send(self, message, binary=False): """ Send a frame over the websocket with message as its payload """ if binary is None: binary = not isinstance(message, six.string_types) opcode = self.OPCODE_BINARY if binary else self.OPCODE_TEXT try: self.send_frame(message, opcode) except WebSocketError: raise WebSocketError("Socket is dead") def close(self, code=1000, message=''): """ Close the websocket and connection, sending the specified code and message. The underlying socket object is _not_ closed, that is the responsibility of the initiator. """ try: message = self._encode_bytes(message) self.send_frame( struct.pack('!H%ds' % len(message), code, message), opcode=self.OPCODE_CLOSE) except WebSocketError: # Failed to write the closing frame but it's ok because we're # closing the socket anyway. logger.debug("Failed to write closing frame -> closing socket") finally: logger.debug("Closed WebSocket") self._closed = True self.stream = None class Stream(object): """ Wraps the handler's socket/rfile attributes and makes it in to a file like object that can be read from/written to by the lower level websocket api. """ __slots__ = ('read', 'write', 'fileno') def __init__(self, wsgi_input): if six.PY2: self.read = wsgi_input._sock.recv self.write = wsgi_input._sock.sendall else: self.read = wsgi_input.raw._sock.recv self.write = wsgi_input.raw._sock.sendall self.fileno = wsgi_input.fileno() class Header(object): __slots__ = ('fin', 'mask', 'opcode', 'flags', 'length') FIN_MASK = 0x80 OPCODE_MASK = 0x0f MASK_MASK = 0x80 LENGTH_MASK = 0x7f RSV0_MASK = 0x40 RSV1_MASK = 0x20 RSV2_MASK = 0x10 # bitwise mask that will determine the reserved bits for a frame header HEADER_FLAG_MASK = RSV0_MASK | RSV1_MASK | RSV2_MASK def __init__(self, fin=0, opcode=0, flags=0, length=0): self.mask = '' self.fin = fin self.opcode = opcode self.flags = flags self.length = length def mask_payload(self, payload): payload = bytearray(payload) mask = bytearray(self.mask) for i in xrange(self.length): payload[i] ^= mask[i % 4] if six.PY3: return bytes(payload) return str(payload) # it's the same operation unmask_payload = mask_payload def __repr__(self): return ("
").format(self.fin, self.opcode, self.length, self.flags, id(self)) @classmethod def decode_header(cls, stream): """ Decode a WebSocket header. :param stream: A file like object that can be 'read' from. :returns: A `Header` instance. """ read = stream.read data = read(2) if len(data) != 2: raise WebSocketError("Unexpected EOF while decoding header") first_byte, second_byte = struct.unpack('!BB', data) header = cls( fin=first_byte & cls.FIN_MASK == cls.FIN_MASK, opcode=first_byte & cls.OPCODE_MASK, flags=first_byte & cls.HEADER_FLAG_MASK, length=second_byte & cls.LENGTH_MASK) has_mask = second_byte & cls.MASK_MASK == cls.MASK_MASK if header.opcode > 0x07: if not header.fin: raise WebSocketError('Received fragmented control frame: {0!r}'.format(data)) # Control frames MUST have a payload length of 125 bytes or less if header.length > 125: raise FrameTooLargeException('Control frame cannot be larger than 125 bytes: {0!r}'.format(data)) if header.length == 126: # 16 bit length data = read(2) if len(data) != 2: raise WebSocketError('Unexpected EOF while decoding header') header.length = struct.unpack('!H', data)[0] elif header.length == 127: # 64 bit length data = read(8) if len(data) != 8: raise WebSocketError('Unexpected EOF while decoding header') header.length = struct.unpack('!Q', data)[0] if has_mask: mask = read(4) if len(mask) != 4: raise WebSocketError('Unexpected EOF while decoding header') header.mask = mask return header @classmethod def encode_header(cls, fin, opcode, mask, length, flags): """ Encodes a WebSocket header. :param fin: Whether this is the final frame for this opcode. :param opcode: The opcode of the payload, see `OPCODE_*` :param mask: Whether the payload is masked. :param length: The length of the frame. :param flags: The RSV* flags. :return: A bytestring encoded header. """ first_byte = opcode second_byte = 0 if six.PY2: extra = '' else: extra = b'' if fin: first_byte |= cls.FIN_MASK if flags & cls.RSV0_MASK: first_byte |= cls.RSV0_MASK if flags & cls.RSV1_MASK: first_byte |= cls.RSV1_MASK if flags & cls.RSV2_MASK: first_byte |= cls.RSV2_MASK # now deal with length complexities if length < 126: second_byte += length elif length <= 0xffff: second_byte += 126 extra = struct.pack('!H', length) elif length <= 0xffffffffffffffff: second_byte += 127 extra = struct.pack('!Q', length) else: raise FrameTooLargeException if mask: second_byte |= cls.MASK_MASK extra += mask if six.PY3: return bytes([first_byte, second_byte]) + extra return chr(first_byte) + chr(second_byte) + extra django-websocket-redis-0.4.7/ws4redis/wsgi_server.py0000644000076500000240000001733213000361613022752 0ustar jriefstaff00000000000000# -*- coding: utf-8 -*- import sys import six from six.moves import http_client from redis import StrictRedis import django if django.VERSION[:2] >= (1, 7): django.setup() from django.conf import settings from django.contrib.auth import get_user from django.core.handlers.wsgi import WSGIRequest, logger from django.core.exceptions import PermissionDenied from django import http from django.utils.encoding import force_str from django.utils.functional import SimpleLazyObject from ws4redis import settings as private_settings from ws4redis.redis_store import RedisMessage from ws4redis.exceptions import WebSocketError, HandshakeError, UpgradeRequiredError try: # django >= 1.8 && python >= 2.7 # https://docs.djangoproject.com/en/1.8/releases/1.7/#django-utils-dictconfig-django-utils-importlib from importlib import import_module except ImportError: # RemovedInDjango19Warning: django.utils.importlib will be removed in Django 1.9. from django.utils.importlib import import_module class WebsocketWSGIServer(object): def __init__(self, redis_connection=None): """ redis_connection can be overriden by a mock object. """ comps = str(private_settings.WS4REDIS_SUBSCRIBER).split('.') module = import_module('.'.join(comps[:-1])) Subscriber = getattr(module, comps[-1]) self.possible_channels = Subscriber.subscription_channels + Subscriber.publish_channels self._redis_connection = redis_connection and redis_connection or StrictRedis(**private_settings.WS4REDIS_CONNECTION) self.Subscriber = Subscriber self._websockets = set() # a list of currently active websockets def assure_protocol_requirements(self, environ): if environ.get('REQUEST_METHOD') != 'GET': raise HandshakeError('HTTP method must be a GET') if environ.get('SERVER_PROTOCOL') != 'HTTP/1.1': raise HandshakeError('HTTP server protocol must be 1.1') if environ.get('HTTP_UPGRADE', '').lower() != 'websocket': raise HandshakeError('Client does not wish to upgrade to a websocket') def process_request(self, request): request.session = None request.user = None session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None) if session_key is not None: engine = import_module(settings.SESSION_ENGINE) request.session = engine.SessionStore(session_key) request.user = SimpleLazyObject(lambda: get_user(request)) def process_subscriptions(self, request): agreed_channels = [] echo_message = False for qp in request.GET: param = qp.strip().lower() if param in self.possible_channels: agreed_channels.append(param) elif param == 'echo': echo_message = True return agreed_channels, echo_message @property def websockets(self): return self._websockets def __call__(self, environ, start_response): """ Hijack the main loop from the original thread and listen on events on the Redis and the Websocket filedescriptors. """ websocket = None subscriber = self.Subscriber(self._redis_connection) try: self.assure_protocol_requirements(environ) request = WSGIRequest(environ) if callable(private_settings.WS4REDIS_PROCESS_REQUEST): private_settings.WS4REDIS_PROCESS_REQUEST(request) else: self.process_request(request) channels, echo_message = self.process_subscriptions(request) if callable(private_settings.WS4REDIS_ALLOWED_CHANNELS): channels = list(private_settings.WS4REDIS_ALLOWED_CHANNELS(request, channels)) elif private_settings.WS4REDIS_ALLOWED_CHANNELS is not None: try: mod, callback = private_settings.WS4REDIS_ALLOWED_CHANNELS.rsplit('.', 1) callback = getattr(import_module(mod), callback, None) if callable(callback): channels = list(callback(request, channels)) except AttributeError: pass websocket = self.upgrade_websocket(environ, start_response) self._websockets.add(websocket) logger.debug('Subscribed to channels: {0}'.format(', '.join(channels))) subscriber.set_pubsub_channels(request, channels) websocket_fd = websocket.get_file_descriptor() listening_fds = [websocket_fd] redis_fd = subscriber.get_file_descriptor() if redis_fd: listening_fds.append(redis_fd) subscriber.send_persited_messages(websocket) recvmsg = None while websocket and not websocket.closed: ready = self.select(listening_fds, [], [], 4.0)[0] if not ready: # flush empty socket websocket.flush() for fd in ready: if fd == websocket_fd: recvmsg = RedisMessage(websocket.receive()) if recvmsg: subscriber.publish_message(recvmsg) elif fd == redis_fd: sendmsg = RedisMessage(subscriber.parse_response()) if sendmsg and (echo_message or sendmsg != recvmsg): websocket.send(sendmsg) else: logger.error('Invalid file descriptor: {0}'.format(fd)) # Check again that the websocket is closed before sending the heartbeat, # because the websocket can closed previously in the loop. if private_settings.WS4REDIS_HEARTBEAT and not websocket.closed: websocket.send(private_settings.WS4REDIS_HEARTBEAT) # Remove websocket from _websockets if closed if websocket.closed: self._websockets.remove(websocket) except WebSocketError as excpt: logger.warning('WebSocketError: {}'.format(excpt), exc_info=sys.exc_info()) response = http.HttpResponse(status=1001, content='Websocket Closed') except UpgradeRequiredError as excpt: logger.info('Websocket upgrade required') response = http.HttpResponseBadRequest(status=426, content=excpt) except HandshakeError as excpt: logger.warning('HandshakeError: {}'.format(excpt), exc_info=sys.exc_info()) response = http.HttpResponseBadRequest(content=excpt) except PermissionDenied as excpt: logger.warning('PermissionDenied: {}'.format(excpt), exc_info=sys.exc_info()) response = http.HttpResponseForbidden(content=excpt) except Exception as excpt: logger.error('Other Exception: {}'.format(excpt), exc_info=sys.exc_info()) response = http.HttpResponseServerError(content=excpt) else: response = http.HttpResponse() finally: subscriber.release() if websocket: websocket.close(code=1001, message='Websocket Closed') else: logger.warning('Starting late response on websocket') status_text = http_client.responses.get(response.status_code, 'UNKNOWN STATUS CODE') status = '{0} {1}'.format(response.status_code, status_text) headers = response._headers.values() if six.PY3: headers = list(headers) start_response(force_str(status), headers) logger.info('Finish non-websocket response with status code: {}'.format(response.status_code)) return response