django-websocket-redis-0.4.5/ 0000755 0000765 0000024 00000000000 12636050205 016273 5 ustar jrief staff 0000000 0000000 django-websocket-redis-0.4.5/django_websocket_redis.egg-info/ 0000755 0000765 0000024 00000000000 12636050205 024463 5 ustar jrief staff 0000000 0000000 django-websocket-redis-0.4.5/django_websocket_redis.egg-info/dependency_links.txt 0000644 0000765 0000024 00000000001 12636050204 030530 0 ustar jrief staff 0000000 0000000
django-websocket-redis-0.4.5/django_websocket_redis.egg-info/not-zip-safe 0000644 0000765 0000024 00000000001 12636050204 026710 0 ustar jrief staff 0000000 0000000
django-websocket-redis-0.4.5/django_websocket_redis.egg-info/PKG-INFO 0000644 0000765 0000024 00000011604 12636050204 025561 0 ustar jrief staff 0000000 0000000 Metadata-Version: 1.1
Name: django-websocket-redis
Version: 0.4.5
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 `__.
Online demo: http://websocket.aws.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 `__ datastore and in a production environment,
it is intended to work under `uWSGI `__
and behind `NGiNX `__ or
`Apache `__
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 `__, 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” `__.
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| |Downloads|
Questions
---------
Please use the issue tracker to ask questions.
License
-------
Copyright © 2015 Jacob Rief.
MIT licensed.
.. |Build Status| image:: https://travis-ci.org/jrief/django-websocket-redis.png?branch=master
:target: https://travis-ci.org/jrief/django-websocket-redis
.. |Downloads| image:: http://img.shields.io/pypi/dm/django-websocket-redis.svg?style=flat-square
:target: https://pypi.python.org/pypi/django-websocket-redis/
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.5/django_websocket_redis.egg-info/requires.txt 0000644 0000765 0000024 00000000214 12636050204 027057 0 ustar jrief staff 0000000 0000000 setuptools
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.5/django_websocket_redis.egg-info/SOURCES.txt 0000644 0000765 0000024 00000001224 12636050204 026345 0 ustar jrief staff 0000000 0000000 LICENSE.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/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.js django-websocket-redis-0.4.5/django_websocket_redis.egg-info/top_level.txt 0000644 0000765 0000024 00000000011 12636050204 027204 0 ustar jrief staff 0000000 0000000 ws4redis
django-websocket-redis-0.4.5/LICENSE.txt 0000644 0000765 0000024 00000002036 12636022307 020121 0 ustar jrief staff 0000000 0000000 Copyright (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.5/MANIFEST.in 0000644 0000765 0000024 00000000173 12636022307 020034 0 ustar jrief staff 0000000 0000000 include LICENSE.txt
include README.md
include setup.py
recursive-include ws4redis *.py
recursive-include ws4redis/static *
django-websocket-redis-0.4.5/PKG-INFO 0000644 0000765 0000024 00000011604 12636050205 017372 0 ustar jrief staff 0000000 0000000 Metadata-Version: 1.1
Name: django-websocket-redis
Version: 0.4.5
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 `__.
Online demo: http://websocket.aws.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 `__ datastore and in a production environment,
it is intended to work under `uWSGI `__
and behind `NGiNX `__ or
`Apache `__
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 `__, 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” `__.
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| |Downloads|
Questions
---------
Please use the issue tracker to ask questions.
License
-------
Copyright © 2015 Jacob Rief.
MIT licensed.
.. |Build Status| image:: https://travis-ci.org/jrief/django-websocket-redis.png?branch=master
:target: https://travis-ci.org/jrief/django-websocket-redis
.. |Downloads| image:: http://img.shields.io/pypi/dm/django-websocket-redis.svg?style=flat-square
:target: https://pypi.python.org/pypi/django-websocket-redis/
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.5/README.md 0000644 0000765 0000024 00000006124 12636023663 017565 0 ustar jrief staff 0000000 0000000 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://websocket.aws.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
------------
[](https://travis-ci.org/jrief/django-websocket-redis)
[](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.5/setup.cfg 0000644 0000765 0000024 00000000073 12636050205 020114 0 ustar jrief staff 0000000 0000000 [egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0
django-websocket-redis-0.4.5/setup.py 0000644 0000765 0000024 00000003477 12636025533 020026 0 ustar jrief staff 0000000 0000000 #!/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.5/ws4redis/ 0000755 0000765 0000024 00000000000 12636050205 020037 5 ustar jrief staff 0000000 0000000 django-websocket-redis-0.4.5/ws4redis/__init__.py 0000644 0000765 0000024 00000000056 12636022307 022153 0 ustar jrief staff 0000000 0000000 # -*- coding: utf-8 -*-
__version__ = '0.4.5'
django-websocket-redis-0.4.5/ws4redis/context_processors.py 0000644 0000765 0000024 00000001041 12636022307 024355 0 ustar jrief staff 0000000 0000000 # -*- 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.5/ws4redis/django_runserver.py 0000644 0000765 0000024 00000006464 12636022307 024002 0 ustar jrief staff 0000000 0000000 #-*- 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)),
]
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.5/ws4redis/exceptions.py 0000644 0000765 0000024 00000001034 12636022307 022572 0 ustar jrief staff 0000000 0000000 #-*- 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.5/ws4redis/models.py 0000644 0000765 0000024 00000001106 12636022307 021674 0 ustar jrief staff 0000000 0000000 # -*- 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.5/ws4redis/publisher.py 0000644 0000765 0000024 00000004631 12636022307 022414 0 ustar jrief staff 0000000 0000000 #-*- coding: utf-8 -*-
from redis import ConnectionPool, StrictRedis
from ws4redis import settings
from ws4redis.redis_store import RedisStore
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 request and request.user and request.user.is_authenticated():
channels.append('{prefix}user:{0}:{facility}'.format(request.user.get_username(), prefix=prefix, facility=facility))
if audience in ('group', 'any',):
try:
if request.user.is_authenticated():
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.5/ws4redis/redis_store.py 0000644 0000765 0000024 00000017745 12636022307 022753 0 ustar jrief staff 0000000 0000000 # -*- coding: utf-8 -*-
import six
import warnings
from ws4redis import settings
"""
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 request and request.user and request.user.is_authenticated():
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 request and request.user and request.user.is_authenticated():
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 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 request and request.user and request.user.is_authenticated():
# 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 request and request.user and request.user.is_authenticated():
# 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.5/ws4redis/settings.py 0000644 0000765 0000024 00000003765 12636022307 022266 0 ustar jrief staff 0000000 0000000 # -*- 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.5/ws4redis/static/ 0000755 0000765 0000024 00000000000 12636050205 021326 5 ustar jrief staff 0000000 0000000 django-websocket-redis-0.4.5/ws4redis/static/js/ 0000755 0000765 0000024 00000000000 12636050205 021742 5 ustar jrief staff 0000000 0000000 django-websocket-redis-0.4.5/ws4redis/static/js/ws4redis.js 0000644 0000765 0000024 00000005241 12636022307 024050 0 ustar jrief staff 0000000 0000000
function WS4Redis(options, $) {
'use strict';
var opts, ws, deferred, timer, attempts = 1;
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 {
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) {
deferred.reject(new Error(err));
}
}
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);
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);
}
}
function on_close(evt) {
console.log("Connection closed!");
if (!timer) {
// try to reconnect
var interval = generateInteval(attempts);
timer = setTimeout(function() {
attempts++;
connect(ws.url);
}, interval);
}
}
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 (typeof 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 generateInteval (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);
};
}
django-websocket-redis-0.4.5/ws4redis/subscriber.py 0000644 0000765 0000024 00000006072 12636022307 022563 0 ustar jrief staff 0000000 0000000 # -*- 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.5/ws4redis/utf8validator.py 0000644 0000765 0000024 00000012333 12636022307 023211 0 ustar jrief staff 0000000 0000000 ###############################################################################
##
## 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.5/ws4redis/uwsgi_runserver.py 0000644 0000765 0000024 00000003034 12636022307 023664 0 ustar jrief staff 0000000 0000000 # -*- 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.5/ws4redis/websocket.py 0000644 0000765 0000024 00000034451 12636022307 022410 0 ustar jrief staff 0000000 0000000 # -*- 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:
rv = str(rv)
code = struct.unpack('!H', 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.5/ws4redis/wsgi_server.py 0000644 0000765 0000024 00000015415 12636022307 022760 0 ustar jrief staff 0000000 0000000 # -*- 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
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
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))
websocket = self.upgrade_websocket(environ, start_response)
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))
if private_settings.WS4REDIS_HEARTBEAT:
websocket.send(private_settings.WS4REDIS_HEARTBEAT)
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