├── .gitignore ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── django_websocket ├── __init__.py ├── decorators.py ├── middleware.py └── websocket.py ├── django_websocket_tests ├── __init__.py ├── manage.py ├── models.py ├── runtests.py ├── settings.py ├── tests.py └── utils.py ├── examples ├── __init__.py ├── manage.py ├── settings.py ├── templates │ └── index.html └── urls.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | dist 3 | build 4 | django_websocket.egg-info 5 | mock-* 6 | django_test_utils-* 7 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Release 0.4.0 5 | ------------- 6 | 7 | - Removed multithreaded development server. Django 1.4 uses multithreading by 8 | default in the ``runserver`` command. 9 | 10 | Release 0.3.0 11 | ------------- 12 | 13 | - Added multithreaded development server. 14 | 15 | Release 0.2.0 16 | ------------- 17 | 18 | - Changed name of attribute ``WebSocket.websocket_closed`` to 19 | ``WebSocket.closed``. 20 | - Changed behaviour of ``WebSocket.close()`` method. Doesn't close system 21 | socket - it's still needed by django! 22 | - You can run tests now with ``python setup.py test``. 23 | - Refactoring ``WebSocket`` class. 24 | - Adding ``WebSocket.read()`` which returns ``None`` if no new messages are 25 | available instead of blocking like ``WebSocket.wait()``. 26 | - Adding example project to play around with. 27 | - Adding ``WebSocket.has_messages()``. You can use it to check if new messages 28 | are ready to be processed. 29 | - Adding ``WebSocket.count_messages()``. 30 | - Removing ``BaseWebSocketMiddleware`` - is replaced by 31 | ``WebSocketMiddleware``. Don't need for a base middleware anymore. We can 32 | integrate everything in one now. 33 | 34 | Release 0.1.1 35 | ------------- 36 | 37 | - Fixed a bug in ``BaseWebSocketMiddleware`` that caused an exception in 38 | ``process_response`` if ``setup_websocket`` failed. Thanks to cedric salaun 39 | for the report. 40 | 41 | Release 0.1.0 42 | ------------- 43 | 44 | - Initial release 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Gregor Müllegger 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | * Neither the name of the author nor the names of other 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES 2 | include LICENSE 3 | include README 4 | recursive-include examples *.py 5 | recursive-include examples *.html 6 | recursive-include django_websocket_tests *.py 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================================== 2 | django-websocket (ABANDONED, do not use) 3 | ======================================== 4 | 5 | **THIS PROJECT IS ABANDONED!** Please use `django-channels`_ to implement 6 | websockets with Django on the server. 7 | 8 | For legacy reasons, you can access the old documentation in the `README of 9 | version 0.3.0 `_. 10 | 11 | .. _django-channels: https://github.com/andrewgodwin/channels/ 12 | .. _readme: https://github.com/gregmuellegger/django-websocket/blob/0.3.0/README 13 | -------------------------------------------------------------------------------- /django_websocket/__init__.py: -------------------------------------------------------------------------------- 1 | from django_websocket.decorators import * 2 | -------------------------------------------------------------------------------- /django_websocket/decorators.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http import HttpResponse 3 | from django.utils.decorators import decorator_from_middleware 4 | from django_websocket.middleware import WebSocketMiddleware 5 | 6 | __all__ = ('accept_websocket', 'require_websocket') 7 | 8 | 9 | WEBSOCKET_MIDDLEWARE_INSTALLED = 'django_websocket.middleware.WebSocketMiddleware' in settings.MIDDLEWARE_CLASSES 10 | 11 | 12 | def _setup_websocket(func): 13 | from functools import wraps 14 | @wraps(func) 15 | def new_func(request, *args, **kwargs): 16 | response = func(request, *args, **kwargs) 17 | if response is None and request.is_websocket(): 18 | return HttpResponse() 19 | return response 20 | if not WEBSOCKET_MIDDLEWARE_INSTALLED: 21 | decorator = decorator_from_middleware(WebSocketMiddleware) 22 | new_func = decorator(new_func) 23 | return new_func 24 | 25 | 26 | def accept_websocket(func): 27 | func.accept_websocket = True 28 | func.require_websocket = getattr(func, 'require_websocket', False) 29 | func = _setup_websocket(func) 30 | return func 31 | 32 | 33 | def require_websocket(func): 34 | func.accept_websocket = True 35 | func.require_websocket = True 36 | func = _setup_websocket(func) 37 | return func 38 | -------------------------------------------------------------------------------- /django_websocket/middleware.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http import HttpResponseBadRequest 3 | from django_websocket.websocket import setup_websocket, MalformedWebSocket 4 | 5 | 6 | WEBSOCKET_ACCEPT_ALL = getattr(settings, 'WEBSOCKET_ACCEPT_ALL', False) 7 | 8 | 9 | class WebSocketMiddleware(object): 10 | def process_request(self, request): 11 | try: 12 | request.websocket = setup_websocket(request) 13 | except MalformedWebSocket, e: 14 | request.websocket = None 15 | request.is_websocket = lambda: False 16 | return HttpResponseBadRequest() 17 | if request.websocket is None: 18 | request.is_websocket = lambda: False 19 | else: 20 | request.is_websocket = lambda: True 21 | 22 | def process_view(self, request, view_func, view_args, view_kwargs): 23 | # open websocket if its an accepted request 24 | if request.is_websocket(): 25 | # deny websocket request if view can't handle websocket 26 | if not WEBSOCKET_ACCEPT_ALL and \ 27 | not getattr(view_func, 'accept_websocket', False): 28 | return HttpResponseBadRequest() 29 | # everything is fine .. so prepare connection by sending handshake 30 | request.websocket.send_handshake() 31 | elif getattr(view_func, 'require_websocket', False): 32 | # websocket was required but not provided 33 | return HttpResponseBadRequest() 34 | 35 | def process_response(self, request, response): 36 | if request.is_websocket() and request.websocket._handshake_sent: 37 | request.websocket._send_closing_frame(True) 38 | return response 39 | -------------------------------------------------------------------------------- /django_websocket/websocket.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import select 3 | import string 4 | import struct 5 | try: 6 | from hashlib import md5 7 | except ImportError: #pragma NO COVER 8 | from md5 import md5 9 | from errno import EINTR 10 | from socket import error as SocketError 11 | 12 | 13 | class MalformedWebSocket(ValueError): 14 | pass 15 | 16 | 17 | def _extract_number(value): 18 | """ 19 | Utility function which, given a string like 'g98sd 5[]221@1', will 20 | return 4926105. Used to parse the Sec-WebSocket-Key headers. 21 | 22 | In other words, it extracts digits from a string and returns the number 23 | due to the number of spaces. 24 | """ 25 | out = "" 26 | spaces = 0 27 | for char in value: 28 | if char in string.digits: 29 | out += char 30 | elif char == " ": 31 | spaces += 1 32 | return int(out) / spaces 33 | 34 | 35 | def setup_websocket(request): 36 | if request.META.get('HTTP_CONNECTION', None) == 'Upgrade' and \ 37 | request.META.get('HTTP_UPGRADE', None) == 'WebSocket': 38 | 39 | # See if they sent the new-format headers 40 | if 'HTTP_SEC_WEBSOCKET_KEY1' in request.META: 41 | protocol_version = 76 42 | if 'HTTP_SEC_WEBSOCKET_KEY2' not in request.META: 43 | raise MalformedWebSocket() 44 | else: 45 | protocol_version = 75 46 | 47 | # If it's new-version, we need to work out our challenge response 48 | if protocol_version == 76: 49 | key1 = _extract_number(request.META['HTTP_SEC_WEBSOCKET_KEY1']) 50 | key2 = _extract_number(request.META['HTTP_SEC_WEBSOCKET_KEY2']) 51 | # There's no content-length header in the request, but it has 8 52 | # bytes of data. 53 | key3 = request.META['wsgi.input'].read(8) 54 | key = struct.pack(">II", key1, key2) + key3 55 | handshake_response = md5(key).digest() 56 | 57 | location = 'ws://%s%s' % (request.get_host(), request.path) 58 | qs = request.META.get('QUERY_STRING') 59 | if qs: 60 | location += '?' + qs 61 | if protocol_version == 75: 62 | handshake_reply = ( 63 | "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" 64 | "Upgrade: WebSocket\r\n" 65 | "Connection: Upgrade\r\n" 66 | "WebSocket-Origin: %s\r\n" 67 | "WebSocket-Location: %s\r\n\r\n" % ( 68 | request.META.get('HTTP_ORIGIN'), 69 | location)) 70 | elif protocol_version == 76: 71 | handshake_reply = ( 72 | "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" 73 | "Upgrade: WebSocket\r\n" 74 | "Connection: Upgrade\r\n" 75 | "Sec-WebSocket-Origin: %s\r\n" 76 | "Sec-WebSocket-Protocol: %s\r\n" 77 | "Sec-WebSocket-Location: %s\r\n" % ( 78 | request.META.get('HTTP_ORIGIN'), 79 | request.META.get('HTTP_SEC_WEBSOCKET_PROTOCOL', 'default'), 80 | location)) 81 | handshake_reply = str(handshake_reply) 82 | handshake_reply = '%s\r\n%s' % (handshake_reply, handshake_response) 83 | 84 | else: 85 | raise MalformedWebSocket("Unknown WebSocket protocol version.") 86 | socket = request.META['wsgi.input']._sock.dup() 87 | return WebSocket( 88 | socket, 89 | protocol=request.META.get('HTTP_WEBSOCKET_PROTOCOL'), 90 | version=protocol_version, 91 | handshake_reply=handshake_reply, 92 | ) 93 | return None 94 | 95 | 96 | class WebSocket(object): 97 | """ 98 | A websocket object that handles the details of 99 | serialization/deserialization to the socket. 100 | 101 | The primary way to interact with a :class:`WebSocket` object is to 102 | call :meth:`send` and :meth:`wait` in order to pass messages back 103 | and forth with the browser. 104 | """ 105 | _socket_recv_bytes = 4096 106 | 107 | 108 | def __init__(self, socket, protocol, version=76, 109 | handshake_reply=None, handshake_sent=None): 110 | ''' 111 | Arguments: 112 | 113 | - ``socket``: An open socket that should be used for WebSocket 114 | communciation. 115 | - ``protocol``: not used yet. 116 | - ``version``: The WebSocket spec version to follow (default is 76) 117 | - ``handshake_reply``: Handshake message that should be sent to the 118 | client when ``send_handshake()`` is called. 119 | - ``handshake_sent``: Whether the handshake is already sent or not. 120 | Set to ``False`` to prevent ``send_handshake()`` to do anything. 121 | ''' 122 | self.socket = socket 123 | self.protocol = protocol 124 | self.version = version 125 | self.closed = False 126 | self.handshake_reply = handshake_reply 127 | if handshake_sent is None: 128 | self._handshake_sent = not bool(handshake_reply) 129 | else: 130 | self._handshake_sent = handshake_sent 131 | self._buffer = "" 132 | self._message_queue = collections.deque() 133 | 134 | def send_handshake(self): 135 | self.socket.sendall(self.handshake_reply) 136 | self._handshake_sent = True 137 | 138 | @classmethod 139 | def _pack_message(cls, message): 140 | """Pack the message inside ``00`` and ``FF`` 141 | 142 | As per the dataframing section (5.3) for the websocket spec 143 | """ 144 | if isinstance(message, unicode): 145 | message = message.encode('utf-8') 146 | elif not isinstance(message, str): 147 | message = str(message) 148 | packed = "\x00%s\xFF" % message 149 | return packed 150 | 151 | def _parse_message_queue(self): 152 | """ Parses for messages in the buffer *buf*. It is assumed that 153 | the buffer contains the start character for a message, but that it 154 | may contain only part of the rest of the message. 155 | 156 | Returns an array of messages, and the buffer remainder that 157 | didn't contain any full messages.""" 158 | msgs = [] 159 | end_idx = 0 160 | buf = self._buffer 161 | while buf: 162 | frame_type = ord(buf[0]) 163 | if frame_type == 0: 164 | # Normal message. 165 | end_idx = buf.find("\xFF") 166 | if end_idx == -1: #pragma NO COVER 167 | break 168 | msgs.append(buf[1:end_idx].decode('utf-8', 'replace')) 169 | buf = buf[end_idx+1:] 170 | elif frame_type == 255: 171 | # Closing handshake. 172 | assert ord(buf[1]) == 0, "Unexpected closing handshake: %r" % buf 173 | self.closed = True 174 | break 175 | else: 176 | raise ValueError("Don't understand how to parse this type of message: %r" % buf) 177 | self._buffer = buf 178 | return msgs 179 | 180 | def send(self, message): 181 | ''' 182 | Send a message to the client. *message* should be convertable to a 183 | string; unicode objects should be encodable as utf-8. 184 | ''' 185 | packed = self._pack_message(message) 186 | self.socket.sendall(packed) 187 | 188 | def _socket_recv(self): 189 | ''' 190 | Gets new data from the socket and try to parse new messages. 191 | ''' 192 | delta = self.socket.recv(self._socket_recv_bytes) 193 | if delta == '': 194 | return False 195 | self._buffer += delta 196 | msgs = self._parse_message_queue() 197 | self._message_queue.extend(msgs) 198 | return True 199 | 200 | def _socket_can_recv(self, timeout=0.0): 201 | ''' 202 | Return ``True`` if new data can be read from the socket. 203 | ''' 204 | r, w, e = [self.socket], [], [] 205 | try: 206 | r, w, e = select.select(r, w, e, timeout) 207 | except select.error, err: 208 | if err.args[0] == EINTR: 209 | return False 210 | raise 211 | return self.socket in r 212 | 213 | def _get_new_messages(self): 214 | # read as long from socket as we need to get a new message. 215 | while self._socket_can_recv(): 216 | self._socket_recv() 217 | if self._message_queue: 218 | return 219 | 220 | def count_messages(self): 221 | ''' 222 | Returns the number of queued messages. 223 | ''' 224 | self._get_new_messages() 225 | return len(self._message_queue) 226 | 227 | def has_messages(self): 228 | ''' 229 | Returns ``True`` if new messages from the socket are available, else 230 | ``False``. 231 | ''' 232 | if self._message_queue: 233 | return True 234 | self._get_new_messages() 235 | if self._message_queue: 236 | return True 237 | return False 238 | 239 | def read(self, fallback=None): 240 | ''' 241 | Return new message or ``fallback`` if no message is available. 242 | ''' 243 | if self.has_messages(): 244 | return self._message_queue.popleft() 245 | return fallback 246 | 247 | def wait(self): 248 | ''' 249 | Waits for and deserializes messages. Returns a single message; the 250 | oldest not yet processed. 251 | ''' 252 | while not self._message_queue: 253 | # Websocket might be closed already. 254 | if self.closed: 255 | return None 256 | # no parsed messages, must mean buf needs more data 257 | new_data = self._socket_recv() 258 | if not new_data: 259 | return None 260 | return self._message_queue.popleft() 261 | 262 | def __iter__(self): 263 | ''' 264 | Use ``WebSocket`` as iterator. Iteration only stops when the websocket 265 | gets closed by the client. 266 | ''' 267 | while True: 268 | message = self.wait() 269 | if message is None: 270 | return 271 | yield message 272 | 273 | def _send_closing_frame(self, ignore_send_errors=False): 274 | ''' 275 | Sends the closing frame to the client, if required. 276 | ''' 277 | if self.version == 76 and not self.closed: 278 | try: 279 | self.socket.sendall("\xff\x00") 280 | except SocketError: 281 | # Sometimes, like when the remote side cuts off the connection, 282 | # we don't care about this. 283 | if not ignore_send_errors: 284 | raise 285 | self.closed = True 286 | 287 | def close(self): 288 | ''' 289 | Forcibly close the websocket. 290 | ''' 291 | self._send_closing_frame() 292 | -------------------------------------------------------------------------------- /django_websocket_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregmuellegger/django-websocket/cb4804e98f397f242e74c6f9e6f4fabab41a7ab7/django_websocket_tests/__init__.py -------------------------------------------------------------------------------- /django_websocket_tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | sys.path.insert(0, 6 | os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 7 | 8 | from django.core.management import execute_manager 9 | try: 10 | import settings # Assumed to be in the same directory. 11 | except ImportError: 12 | import sys 13 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 14 | sys.exit(1) 15 | 16 | if __name__ == "__main__": 17 | execute_manager(settings) 18 | -------------------------------------------------------------------------------- /django_websocket_tests/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregmuellegger/django-websocket/cb4804e98f397f242e74c6f9e6f4fabab41a7ab7/django_websocket_tests/models.py -------------------------------------------------------------------------------- /django_websocket_tests/runtests.py: -------------------------------------------------------------------------------- 1 | #This file mainly exists to allow python setup.py test to work. 2 | import os, sys 3 | os.environ['DJANGO_SETTINGS_MODULE'] = 'django_websocket_tests.settings' 4 | test_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 | sys.path.insert(0, test_dir) 6 | 7 | import django 8 | from django.test.utils import get_runner 9 | from django.conf import settings 10 | 11 | def runtests(): 12 | TestRunner = get_runner(settings) 13 | if django.VERSION[:2] > (1,1): 14 | test_runner = TestRunner(verbosity=1, interactive=True) 15 | failures = test_runner.run_tests(settings.TEST_APPS) 16 | else: 17 | # test runner is not class based, this means we use django 1.1.x or 18 | # earlier. 19 | failures = TestRunner(settings.TEST_APPS, verbosity=1, interactive=True) 20 | sys.exit(bool(failures)) 21 | 22 | if __name__ == '__main__': 23 | runtests() 24 | -------------------------------------------------------------------------------- /django_websocket_tests/settings.py: -------------------------------------------------------------------------------- 1 | DEBUG = True 2 | TEMPLATE_DEBUG = DEBUG 3 | 4 | ADMINS = ( 5 | # ('Your Name', 'your_email@domain.com'), 6 | ) 7 | 8 | MANAGERS = ADMINS 9 | 10 | DATABASE_ENGINE = 'sqlite3' 11 | DATABASES = { 12 | 'default': { 13 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 14 | 'NAME': 'db.sqlite', # Or path to database file if using sqlite3. 15 | 'USER': '', # Not used with sqlite3. 16 | 'PASSWORD': '', # Not used with sqlite3. 17 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 18 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 19 | } 20 | } 21 | 22 | # Local time zone for this installation. Choices can be found here: 23 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 24 | # although not all choices may be available on all operating systems. 25 | # On Unix systems, a value of None will cause Django to use the same 26 | # timezone as the operating system. 27 | # If running in a Windows environment this must be set to the same as your 28 | # system time zone. 29 | TIME_ZONE = 'America/Chicago' 30 | 31 | # Language code for this installation. All choices can be found here: 32 | # http://www.i18nguy.com/unicode/language-identifiers.html 33 | LANGUAGE_CODE = 'en-us' 34 | 35 | SITE_ID = 1 36 | 37 | # If you set this to False, Django will make some optimizations so as not 38 | # to load the internationalization machinery. 39 | USE_I18N = True 40 | 41 | # If you set this to False, Django will not format dates, numbers and 42 | # calendars according to the current locale 43 | USE_L10N = True 44 | 45 | # Absolute path to the directory that holds media. 46 | # Example: "/home/media/media.lawrence.com/" 47 | MEDIA_ROOT = '' 48 | 49 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 50 | # trailing slash if there is a path component (optional in other cases). 51 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 52 | MEDIA_URL = '' 53 | 54 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 55 | # trailing slash. 56 | # Examples: "http://foo.com/media/", "/media/". 57 | ADMIN_MEDIA_PREFIX = '/media/' 58 | 59 | # Make this unique, and don't share it with anybody. 60 | SECRET_KEY = '' 61 | 62 | # List of callables that know how to import templates from various sources. 63 | TEMPLATE_LOADERS = ( 64 | 'django.template.loaders.filesystem.Loader', 65 | 'django.template.loaders.app_directories.Loader', 66 | # 'django.template.loaders.eggs.Loader', 67 | ) 68 | 69 | MIDDLEWARE_CLASSES = ( 70 | 'django.middleware.common.CommonMiddleware', 71 | 'django.contrib.sessions.middleware.SessionMiddleware', 72 | 'django.middleware.csrf.CsrfViewMiddleware', 73 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 74 | ) 75 | 76 | ROOT_URLCONF = 'django_websocket_tests.urls' 77 | 78 | TEMPLATE_DIRS = ( 79 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 80 | # Always use forward slashes, even on Windows. 81 | # Don't forget to use absolute paths, not relative paths. 82 | ) 83 | 84 | INSTALLED_APPS = ( 85 | 'django_websocket_tests', 86 | 'django_websocket', 87 | 88 | 'django.contrib.auth', 89 | 'django.contrib.contenttypes', 90 | 'django.contrib.sessions', 91 | 'django.contrib.sites', 92 | # Uncomment the next line to enable the admin: 93 | # 'django.contrib.admin', 94 | ) 95 | 96 | TEST_APPS = ( 97 | 'django_websocket_tests', 98 | ) 99 | -------------------------------------------------------------------------------- /django_websocket_tests/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from mock import Mock 3 | from django.core.urlresolvers import reverse 4 | from django.contrib.auth.models import User 5 | from django.http import HttpResponse 6 | from django.test import TestCase 7 | from django.test.client import RequestFactory 8 | from django_websocket.decorators import accept_websocket, require_websocket 9 | from django_websocket.websocket import WebSocket 10 | 11 | 12 | class WebSocketTests(TestCase): 13 | def setUp(self): 14 | self.socket = Mock() 15 | self.protocol = '1' 16 | 17 | def test_send_handshake(self): 18 | handshake = 'Hi!' 19 | ws = WebSocket(self.socket, self.protocol, handshake_reply=handshake) 20 | self.assertEquals(ws._handshake_sent, False) 21 | ws.send_handshake() 22 | self.assertEquals(self.socket.sendall.call_count, 1) 23 | self.assertEquals(self.socket.sendall.call_args, ((handshake,), {})) 24 | 25 | def test_message_sending(self): 26 | ws = WebSocket(self.socket, self.protocol) 27 | ws.send('foobar') 28 | self.assertEquals(self.socket.sendall.call_count, 1) 29 | self.assertEquals(self.socket.sendall.call_args, (('\x00foobar\xFF',), {})) 30 | message = self.socket.sendall.call_args[0][0] 31 | self.assertEquals(type(message), str) 32 | 33 | ws.send(u'Küss die Hand schöne Frau') 34 | self.assertEquals(self.socket.sendall.call_count, 2) 35 | self.assertEquals(self.socket.sendall.call_args, (('\x00K\xc3\xbcss die Hand sch\xc3\xb6ne Frau\xFF',), {})) 36 | message = self.socket.sendall.call_args[0][0] 37 | self.assertEquals(type(message), str) 38 | 39 | def test_message_receiving(self): 40 | ws = WebSocket(self.socket, self.protocol) 41 | self.assertFalse(ws.closed) 42 | 43 | results = [ 44 | '\x00spam & eggs\xFF', 45 | '\x00K\xc3\xbcss die Hand sch\xc3\xb6ne Frau\xFF', 46 | '\xFF\x00'][::-1] 47 | def return_results(*args, **kwargs): 48 | return results.pop() 49 | self.socket.recv.side_effect = return_results 50 | self.assertEquals(ws.wait(), u'spam & eggs') 51 | self.assertEquals(ws.wait(), u'Küss die Hand schöne Frau') 52 | 53 | def test_closing_socket_by_client(self): 54 | self.socket.recv.return_value = '\xFF\x00' 55 | 56 | ws = WebSocket(self.socket, self.protocol) 57 | self.assertFalse(ws.closed) 58 | self.assertEquals(ws.wait(), None) 59 | self.assertTrue(ws.closed) 60 | 61 | self.assertEquals(self.socket.shutdown.call_count, 0) 62 | self.assertEquals(self.socket.close.call_count, 0) 63 | 64 | def test_closing_socket_by_server(self): 65 | ws = WebSocket(self.socket, self.protocol) 66 | self.assertFalse(ws.closed) 67 | ws.close() 68 | self.assertEquals(self.socket.sendall.call_count, 1) 69 | self.assertEquals(self.socket.sendall.call_args, (('\xFF\x00',), {})) 70 | # don't close system socket! django still needs it. 71 | self.assertEquals(self.socket.shutdown.call_count, 0) 72 | self.assertEquals(self.socket.close.call_count, 0) 73 | self.assertTrue(ws.closed) 74 | 75 | # closing again will not send another close message 76 | ws.close() 77 | self.assertTrue(ws.closed) 78 | self.assertEquals(self.socket.sendall.call_count, 1) 79 | self.assertEquals(self.socket.shutdown.call_count, 0) 80 | self.assertEquals(self.socket.close.call_count, 0) 81 | 82 | def test_iterator_behaviour(self): 83 | results = [ 84 | '\x00spam & eggs\xFF', 85 | '\x00K\xc3\xbcss die Hand sch\xc3\xb6ne Frau\xFF', 86 | '\xFF\x00'][::-1] 87 | expected_results = [ 88 | u'spam & eggs', 89 | u'Küss die Hand schöne Frau'] 90 | def return_results(*args, **kwargs): 91 | return results.pop() 92 | self.socket.recv.side_effect = return_results 93 | 94 | ws = WebSocket(self.socket, self.protocol) 95 | for i, message in enumerate(ws): 96 | self.assertEquals(message, expected_results[i]) 97 | 98 | 99 | @accept_websocket 100 | def add_one(request): 101 | if request.is_websocket(): 102 | for message in request.websocket: 103 | request.websocket.send(int(message) + 1) 104 | else: 105 | value = int(request.GET['value']) 106 | value += 1 107 | return HttpResponse(unicode(value)) 108 | 109 | 110 | @require_websocket 111 | def echo_once(request): 112 | request.websocket.send(request.websocket.wait()) 113 | 114 | 115 | class DecoratorTests(TestCase): 116 | def setUp(self): 117 | self.rf = RequestFactory() 118 | 119 | def test_require_websocket_decorator(self): 120 | # view requires websocket -> bad request 121 | request = self.rf.get('/echo/') 122 | response = echo_once(request) 123 | self.assertEquals(response.status_code, 400) 124 | 125 | def test_accept_websocket_decorator(self): 126 | request = self.rf.get('/add/', {'value': '23'}) 127 | response = add_one(request) 128 | self.assertEquals(response.status_code, 200) 129 | self.assertEquals(response.content, '24') 130 | 131 | # TODO: test views with actual websocket connection - not really possible yet 132 | # with django's test client/request factory. Heavy use of mock objects 133 | # necessary. 134 | -------------------------------------------------------------------------------- /django_websocket_tests/utils.py: -------------------------------------------------------------------------------- 1 | from django.test import Client 2 | from django.core.handlers.wsgi import WSGIRequest 3 | 4 | 5 | class RequestFactory(Client): 6 | """ 7 | Class that lets you create mock Request objects for use in testing. 8 | 9 | Usage: 10 | 11 | rf = RequestFactory() 12 | get_request = rf.get('/hello/') 13 | post_request = rf.post('/submit/', {'foo': 'bar'}) 14 | 15 | This class re-uses the django.test.client.Client interface, docs here: 16 | http://www.djangoproject.com/documentation/testing/#the-test-client 17 | 18 | Once you have a request object you can pass it to any view function, 19 | just as if that view had been hooked up using a URLconf. 20 | 21 | """ 22 | def request(self, **request): 23 | """ 24 | Similar to parent class, but returns the request object as soon as it 25 | has created it. 26 | """ 27 | environ = { 28 | 'HTTP_COOKIE': self.cookies, 29 | 'PATH_INFO': '/', 30 | 'QUERY_STRING': '', 31 | 'REQUEST_METHOD': 'GET', 32 | 'SCRIPT_NAME': '', 33 | 'SERVER_NAME': 'testserver', 34 | 'SERVER_PORT': 80, 35 | 'SERVER_PROTOCOL': 'HTTP/1.1', 36 | } 37 | environ.update(self.defaults) 38 | environ.update(request) 39 | return WSGIRequest(environ) 40 | 41 | 42 | class WebsocketFactory(RequestFactory): 43 | def __init__(self, *args, **kwargs): 44 | self.protocol_version = kwargs.pop('websocket_version', 75) 45 | super(WebsocketFactory, self).__init__(*args, **kwargs) 46 | 47 | def request(self, **request): 48 | """ 49 | Returns a request simliar to one from a browser which wants to upgrade 50 | to a websocket connection. 51 | """ 52 | environ = { 53 | 'HTTP_COOKIE': self.cookies, 54 | 'PATH_INFO': '/', 55 | 'QUERY_STRING': '', 56 | 'REQUEST_METHOD': 'GET', 57 | 'SCRIPT_NAME': '', 58 | 'SERVER_NAME': 'testserver', 59 | 'SERVER_PORT': 80, 60 | 'SERVER_PROTOCOL': 'HTTP/1.1', 61 | # WebSocket specific headers 62 | 'HTTP_CONNECTION': 'Upgrade', 63 | 'HTTP_UPGRADE': 'WebSocket', 64 | } 65 | if self.protocol_version == 76: 66 | raise NotImplementedError(u'This version is not yet supported.') 67 | environ.update(self.defaults) 68 | environ.update(request) 69 | return WSGIRequest(environ) 70 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregmuellegger/django-websocket/cb4804e98f397f242e74c6f9e6f4fabab41a7ab7/examples/__init__.py -------------------------------------------------------------------------------- /examples/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /examples/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for examples project. 2 | import os 3 | import sys 4 | PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) 5 | sys.path.insert(0, os.path.dirname(PROJECT_ROOT)) 6 | 7 | DEBUG = True 8 | TEMPLATE_DEBUG = DEBUG 9 | 10 | ADMINS = ( 11 | # ('Your Name', 'your_email@domain.com'), 12 | ) 13 | 14 | MANAGERS = ADMINS 15 | 16 | DATABASES = { 17 | 'default': { 18 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 19 | 'NAME': 'db.sqlite', # Or path to database file if using sqlite3. 20 | 'USER': '', # Not used with sqlite3. 21 | 'PASSWORD': '', # Not used with sqlite3. 22 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 23 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 24 | } 25 | } 26 | 27 | # Local time zone for this installation. Choices can be found here: 28 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 29 | # although not all choices may be available on all operating systems. 30 | # On Unix systems, a value of None will cause Django to use the same 31 | # timezone as the operating system. 32 | # If running in a Windows environment this must be set to the same as your 33 | # system time zone. 34 | TIME_ZONE = 'America/Chicago' 35 | 36 | # Language code for this installation. All choices can be found here: 37 | # http://www.i18nguy.com/unicode/language-identifiers.html 38 | LANGUAGE_CODE = 'en-us' 39 | 40 | SITE_ID = 1 41 | 42 | # If you set this to False, Django will make some optimizations so as not 43 | # to load the internationalization machinery. 44 | USE_I18N = True 45 | 46 | # If you set this to False, Django will not format dates, numbers and 47 | # calendars according to the current locale 48 | USE_L10N = True 49 | 50 | # Absolute path to the directory that holds media. 51 | # Example: "/home/media/media.lawrence.com/" 52 | MEDIA_ROOT = '' 53 | 54 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 55 | # trailing slash if there is a path component (optional in other cases). 56 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 57 | MEDIA_URL = '' 58 | 59 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 60 | # trailing slash. 61 | # Examples: "http://foo.com/media/", "/media/". 62 | ADMIN_MEDIA_PREFIX = '/media/' 63 | 64 | # Make this unique, and don't share it with anybody. 65 | SECRET_KEY = 'a#(pgz7yyyld!7mgs3(yve=t0^!psep_-&w=e@0&p)a##s(&r-' 66 | 67 | # List of callables that know how to import templates from various sources. 68 | TEMPLATE_LOADERS = ( 69 | 'django.template.loaders.filesystem.Loader', 70 | 'django.template.loaders.app_directories.Loader', 71 | # 'django.template.loaders.eggs.Loader', 72 | ) 73 | 74 | MIDDLEWARE_CLASSES = ( 75 | 'django.middleware.common.CommonMiddleware', 76 | 'django.contrib.sessions.middleware.SessionMiddleware', 77 | 'django.middleware.csrf.CsrfViewMiddleware', 78 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 79 | 'django.contrib.messages.middleware.MessageMiddleware', 80 | ) 81 | 82 | ROOT_URLCONF = 'examples.urls' 83 | 84 | TEMPLATE_DIRS = ( 85 | os.path.join(PROJECT_ROOT, 'templates'), 86 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 87 | # Always use forward slashes, even on Windows. 88 | # Don't forget to use absolute paths, not relative paths. 89 | ) 90 | 91 | INSTALLED_APPS = ( 92 | 'django_websocket', 93 | 94 | 'django.contrib.auth', 95 | 'django.contrib.contenttypes', 96 | 'django.contrib.sessions', 97 | 'django.contrib.sites', 98 | 'django.contrib.messages', 99 | # Uncomment the next line to enable the admin: 100 | # 'django.contrib.admin', 101 | ) 102 | -------------------------------------------------------------------------------- /examples/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | django-websocket 5 | 6 | 23 | 24 | 25 | 26 | 27 |

Received Messages

28 |
29 | 30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from django.shortcuts import render_to_response 3 | from django.template import RequestContext 4 | from django_websocket import require_websocket 5 | 6 | # Uncomment the next two lines to enable the admin: 7 | # from django.contrib import admin 8 | # admin.autodiscover() 9 | 10 | def base_view(request): 11 | return render_to_response('index.html', { 12 | 13 | }, context_instance=RequestContext(request)) 14 | 15 | 16 | @require_websocket 17 | def echo(request): 18 | for message in request.websocket: 19 | request.websocket.send(message) 20 | 21 | 22 | urlpatterns = patterns('', 23 | # Example: 24 | url(r'^$', base_view), 25 | url(r'^echo$', echo), 26 | 27 | # Uncomment the admin/doc line below and add 'django.contrib.admindocs' 28 | # to INSTALLED_APPS to enable admin documentation: 29 | # (r'^admin/doc/', include('django.contrib.admindocs.urls')), 30 | 31 | # Uncomment the next line to enable the admin: 32 | # (r'^admin/', include(admin.site.urls)), 33 | ) 34 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.4.1 2 | mock==0.8.0 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from setuptools import setup 4 | 5 | 6 | class UltraMagicString(object): 7 | ''' 8 | Taken from 9 | http://stackoverflow.com/questions/1162338/whats-the-right-way-to-use-unicode-metadata-in-setup-py 10 | ''' 11 | def __init__(self, value): 12 | self.value = value 13 | 14 | def __str__(self): 15 | return self.value 16 | 17 | def __unicode__(self): 18 | return self.value.decode('UTF-8') 19 | 20 | def __add__(self, other): 21 | return UltraMagicString(self.value + str(other)) 22 | 23 | def split(self, *args, **kw): 24 | return self.value.split(*args, **kw) 25 | 26 | 27 | long_description = UltraMagicString('\n\n'.join(( 28 | file('README.rst').read(), 29 | file('CHANGES.rst').read(), 30 | ))) 31 | 32 | 33 | setup( 34 | name = u'django-websocket', 35 | version = u'0.4.0pre1', 36 | url = u'http://pypi.python.org/pypi/django-websocket', 37 | license = u'BSD', 38 | description = u'Websocket support for django.', 39 | long_description = long_description, 40 | author = UltraMagicString('Gregor Müllegger'), 41 | author_email = u'gregor@muellegger.de', 42 | packages = ['django_websocket'], 43 | classifiers = [ 44 | 'Development Status :: 3 - Alpha', 45 | 'Environment :: Web Environment', 46 | 'Framework :: Django', 47 | 'Intended Audience :: Developers', 48 | 'License :: OSI Approved :: BSD License', 49 | 'Operating System :: OS Independent', 50 | 'Programming Language :: Python', 51 | 'Topic :: Software Development :: Libraries :: Python Modules', 52 | 'Topic :: Utilities' 53 | ], 54 | zip_safe = True, 55 | install_requires = ['setuptools'], 56 | 57 | test_suite = 'django_websocket_tests.runtests.runtests', 58 | tests_require=[ 59 | 'django-test-utils', 60 | 'mock', 61 | ], 62 | ) 63 | --------------------------------------------------------------------------------