163 lines
5.9 KiB
Python
163 lines
5.9 KiB
Python
# -*- 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']
|
|
|
|
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
|
|
|
|
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 []
|