Source code for ws4py.server.wsgiutils

# -*- coding: utf-8 -*-
__doc__ = """
This module provides a WSGI application suitable
for a WSGI server such as gevent or wsgiref for instance.

:pep:`333` couldn't foresee a protocol such as
WebSockets but luckily the way the initial
protocol upgrade was designed means that we can
fit the handshake in a WSGI flow.

The handshake validates the request against
some internal or user-provided values and
fails the request if the validation doesn't
complete.

On success, the provided WebSocket subclass
is instanciated and stored into the
`'ws4py.websocket'` environ key so that
the WSGI server can handle it.

The WSGI application returns an empty iterable
since there is little value to return some
content within the response to the handshake.

A server wishing to support WebSocket via ws4py
should:

- Provide the real socket object to ws4py through the
  `'ws4py.socket'` environ key. We can't use `'wsgi.input'`
  as it may be wrapper to the socket we wouldn't know
  how to extract the socket from.
- Look for the `'ws4py.websocket'` key in the environ
  when the application has returned and probably attach
  it to a :class:`ws4py.manager.WebSocketManager` instance
  so that the websocket runs its life.
- Remove the `'ws4py.websocket'` and `'ws4py.socket'`
  environ keys once the application has returned.
  No need for these keys to persist.
- Not close the underlying socket otherwise, well,
  your websocket will also shutdown.

.. warning::

  The WSGI application sets the `'Upgrade'` header response
  as specified by :rfc:`6455`. This is not tolerated by
  :pep:`333` since it's a hop-by-hop header.
  We expect most servers won't mind.
"""
import base64
from hashlib import sha1
import logging
import sys

from ws4py.websocket import WebSocket
from ws4py.exc import HandshakeError
from ws4py.compat import unicode, py3k
from ws4py import WS_VERSION, WS_KEY, format_addresses

logger = logging.getLogger('ws4py')

__all__ = ['WebSocketWSGIApplication']

[docs]class WebSocketWSGIApplication(object): def __init__(self, protocols=None, extensions=None, handler_cls=WebSocket): """ WSGI application usable to complete the upgrade handshake by validating the requested protocols and extensions as well as the websocket version. If the upgrade validates, the `handler_cls` class is instanciated and stored inside the WSGI `environ` under the `'ws4py.websocket'` key to make it available to the WSGI handler. """ self.protocols = protocols self.extensions = extensions self.handler_cls = handler_cls
[docs] def make_websocket(self, sock, protocols, extensions, environ): """ Initialize the `handler_cls` instance with the given negociated sets of protocols and extensions as well as the `environ` and `sock`. Stores then the instance in the `environ` dict under the `'ws4py.websocket'` key. """ websocket = self.handler_cls(sock, protocols, extensions, environ.copy()) environ['ws4py.websocket'] = websocket return websocket
def __call__(self, environ, start_response): if environ.get('REQUEST_METHOD') != 'GET': raise HandshakeError('HTTP method must be a GET') for key, expected_value in [('HTTP_UPGRADE', 'websocket'), ('HTTP_CONNECTION', 'upgrade')]: actual_value = environ.get(key, '').lower() if not actual_value: raise HandshakeError('Header %s is not defined' % key) if expected_value not in actual_value: raise HandshakeError('Illegal value for header %s: %s' % (key, actual_value)) key = environ.get('HTTP_SEC_WEBSOCKET_KEY') if key: ws_key = base64.b64decode(key.encode('utf-8')) if len(ws_key) != 16: raise HandshakeError("WebSocket key's length is invalid") version = environ.get('HTTP_SEC_WEBSOCKET_VERSION') supported_versions = b', '.join([unicode(v).encode('utf-8') for v in WS_VERSION]) version_is_valid = False if version: try: version = int(version) except: pass else: version_is_valid = version in WS_VERSION if not version_is_valid: environ['websocket.version'] = unicode(version).encode('utf-8') raise HandshakeError('Unhandled or missing WebSocket version') ws_protocols = [] protocols = self.protocols or [] subprotocols = environ.get('HTTP_SEC_WEBSOCKET_PROTOCOL') if subprotocols: for s in subprotocols.split(','): s = s.strip() if s in protocols: ws_protocols.append(s) ws_extensions = [] exts = self.extensions or [] extensions = environ.get('HTTP_SEC_WEBSOCKET_EXTENSIONS') if extensions: for ext in extensions.split(','): ext = ext.strip() if ext in exts: ws_extensions.append(ext) accept_value = base64.b64encode(sha1(key.encode('utf-8') + WS_KEY).digest()) if py3k: accept_value = accept_value.decode('utf-8') upgrade_headers = [ ('Upgrade', 'websocket'), ('Connection', 'Upgrade'), ('Sec-WebSocket-Version', '%s' % version), ('Sec-WebSocket-Accept', accept_value), ] if ws_protocols: upgrade_headers.append(('Sec-WebSocket-Protocol', ', '.join(ws_protocols))) if ws_extensions: upgrade_headers.append(('Sec-WebSocket-Extensions', ','.join(ws_extensions))) start_response("101 Switching Protocols", upgrade_headers) self.make_websocket(environ['ws4py.socket'], ws_protocols, ws_extensions, environ) return []