Skip to content

Commit 853cef1

Browse files
committed
made the WSGI app common enough for various servers and added a wsgiref server example
1 parent 34051db commit 853cef1

3 files changed

Lines changed: 312 additions & 116 deletions

File tree

ws4py/server/geventserver.py

Lines changed: 12 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,30 @@
44
55
Its usage is rather simple:
66
7-
>>> from gevent import monkey; monkey.patch_all()
8-
>>> from ws4py.websocket import EchoWebSocket
9-
>>> from ws4py.server.geventserver import WebSocketWSGIApplication, WSGIServer
7+
.. code-block: python
108
11-
>>> server = WSGIServer(('localhost', 9000), WebSocketWSGIApplication(handler_cls=EchoWebSocket))
12-
>>> server.serve_forever()
9+
from gevent import monkey; monkey.patch_all()
10+
from ws4py.websocket import EchoWebSocket
11+
from ws4py.server.geventserver import WSGIServer
12+
from ws4py.server.wsgiutils import WebSocketWSGIApplication
13+
14+
server = WSGIServer(('localhost', 9000), WebSocketWSGIApplication(handler_cls=EchoWebSocket))
15+
server.serve_forever()
1316
1417
"""
15-
import base64
16-
from hashlib import sha1
1718
import logging
18-
import signal
1919
import sys
2020

2121
import gevent
2222
from gevent.pywsgi import WSGIHandler, WSGIServer as _WSGIServer
2323
from gevent.pool import Group
2424

25-
from ws4py.websocket import WebSocket
26-
from ws4py.exc import HandshakeError
27-
from ws4py.compat import enc
28-
from ws4py import WS_VERSION, WS_KEY, format_addresses
25+
from ws4py import format_addresses
26+
from ws4py.server.wsgiutils import WebSocketWSGIApplication
2927

3028
logger = logging.getLogger('ws4py')
3129

32-
__all__ = ['WebSocketWSGIHandler', 'WebSocketWSGIApplication', 'WSGIServer']
30+
__all__ = ['WebSocketWSGIHandler', 'WSGIServer']
3331

3432
class WebSocketWSGIHandler(WSGIHandler):
3533
"""
@@ -45,19 +43,6 @@ def run_application(self):
4543
upgrade_header = self.environ.get('HTTP_UPGRADE', '').lower()
4644
if upgrade_header:
4745
try:
48-
# A couple of dummy validations
49-
if self.environ.get('REQUEST_METHOD') != 'GET':
50-
raise HandshakeError('HTTP method must be a GET')
51-
52-
for key, expected_value in [('HTTP_UPGRADE', 'websocket'),
53-
('HTTP_CONNECTION', 'upgrade')]:
54-
actual_value = self.environ.get(key, '').lower()
55-
if not actual_value:
56-
raise HandshakeError('Header %s is not defined' % key)
57-
if expected_value not in actual_value:
58-
raise HandshakeError('Illegal value for header %s: %s' %
59-
(key, actual_value))
60-
6146
# Build and start the HTTP response
6247
self.environ['ws4py.socket'] = self.socket or self.environ['wsgi.input'].rfile._sock
6348
self.result = self.application(self.environ, self.start_response) or []
@@ -75,95 +60,6 @@ def run_application(self):
7560
else:
7661
gevent.pywsgi.WSGIHandler.run_application(self)
7762

78-
class WebSocketWSGIApplication(object):
79-
def __init__(self, protocols=None, extensions=None,
80-
version=WS_VERSION, handler_cls=WebSocket):
81-
"""
82-
WSGI application usable to complete the upgrade handshake
83-
by validating the requested protocols and extensions as
84-
well as the websocket version.
85-
86-
If the upgrade validates, the `handler_cls` class
87-
is instanciated and stored inside the WSGI `environ`
88-
under the `'ws4y.websocket'` key to make it
89-
available to the WSGI handler.
90-
91-
Note that the key and its associated value will be removed
92-
from the environ dictionary from within the
93-
WSGI handler.
94-
"""
95-
self.protocols = protocols
96-
self.extensions = extensions
97-
self.version = version
98-
self.handler_cls = handler_cls
99-
100-
def build_websocket(self, sock, protocols, extensions, environ):
101-
"""
102-
Initialize the `handler_cls` instance with the given
103-
negociated sets of protocols and extensions as well as
104-
the `environ` and `sock`.
105-
106-
Stores then the instance in the `environ` dict
107-
under the `'ws4y.websocket'` key.
108-
"""
109-
websocket = self.handler_cls(sock, protocols, extensions,
110-
environ.copy())
111-
environ['ws4y.websocket'] = websocket
112-
return websocket
113-
114-
def __call__(self, environ, start_response):
115-
key = environ.get('HTTP_SEC_WEBSOCKET_KEY')
116-
if key:
117-
ws_key = base64.b64decode(enc(key))
118-
if len(ws_key) != 16:
119-
raise HandshakeError("WebSocket key's length is invalid")
120-
121-
version = environ.get('HTTP_SEC_WEBSOCKET_VERSION')
122-
supported_versions = ', '.join([str(v) for v in self.version])
123-
version_is_valid = False
124-
if version:
125-
try: version = int(version)
126-
except: pass
127-
else: version_is_valid = version in WS_VERSION
128-
129-
if not version_is_valid:
130-
environ['websocket.version'] = str(version)
131-
raise HandshakeError('Unhandled or missing WebSocket version')
132-
133-
ws_protocols = []
134-
protocols = self.protocols or []
135-
subprotocols = environ.get('HTTP_SEC_WEBSOCKET_PROTOCOL')
136-
if subprotocols:
137-
for s in subprotocols.split(','):
138-
s = s.strip()
139-
if s in protocols:
140-
ws_protocols.append(s)
141-
142-
ws_extensions = []
143-
exts = self.extensions or []
144-
extensions = environ.get('HTTP_SEC_WEBSOCKET_EXTENSIONS')
145-
if extensions:
146-
for ext in extensions.split(','):
147-
ext = ext.strip()
148-
if ext in exts:
149-
ws_extensions.append(ext)
150-
151-
upgrade_headers = [
152-
('Upgrade', 'websocket'),
153-
('Connection', 'Upgrade'),
154-
('Sec-WebSocket-Version', str(version)),
155-
('Sec-WebSocket-Accept', base64.b64encode(sha1(key + WS_KEY).digest())),
156-
]
157-
if ws_protocols:
158-
upgrade_headers.append(('Sec-WebSocket-Protocol', ', '.join(ws_protocols)))
159-
if ws_extensions:
160-
upgrade_headers.append(('Sec-WebSocket-Extensions', ','.join(ws_extensions)))
161-
162-
start_response("101 Switching Protocols", upgrade_headers)
163-
164-
self.build_websocket(environ['ws4py.socket'], ws_protocols,
165-
ws_extensions, environ)
166-
16763
class WSGIServer(_WSGIServer):
16864
handler_class = WebSocketWSGIHandler
16965

@@ -200,6 +96,6 @@ def stop(self, *args, **kwargs):
20096
configure_logger()
20197

20298
from ws4py.websocket import EchoWebSocket
203-
server = WSGIServer(('127.0.0.1', 9001),
99+
server = WSGIServer(('127.0.0.1', 9000),
204100
WebSocketWSGIApplication(handler_cls=EchoWebSocket))
205101
server.serve_forever()

ws4py/server/wsgirefserver.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# -*- coding: utf-8 -*-
2+
__doc__ = """
3+
Add WebSocket support to the built-in WSGI server
4+
provided by the :py:mod:`wsgiref`. This is clearly not
5+
meant to be a production server so please consider this
6+
only for testing purpose.
7+
8+
Mostly, this module overrides bits and pieces of
9+
the built-in classes so that it supports the WebSocket
10+
workflow.
11+
12+
.. code-block: python
13+
14+
from wsgiref.simple_server import make_server
15+
from ws4py.websocket import EchoWebSocket
16+
from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler
17+
from ws4py.server.wsgiutils import WebSocketWSGIApplication
18+
19+
server = make_server('', 9000, server_class=WSGIServer,
20+
handler_class=WebSocketWSGIRequestHandler,
21+
app=WebSocketWSGIApplication(handler_cls=EchoWebSocket))
22+
server.initialize_websockets_manager()
23+
server.serve_forever()
24+
"""
25+
import logging
26+
import sys
27+
from wsgiref.handlers import SimpleHandler
28+
from wsgiref.simple_server import WSGIRequestHandler, WSGIServer as _WSGIServer
29+
from wsgiref import util
30+
31+
util._hoppish = {}.__contains__
32+
33+
from ws4py.manager import WebSocketManager
34+
from ws4py import format_addresses
35+
from ws4py.server.wsgiutils import WebSocketWSGIApplication
36+
37+
__all__ = ['WebSocketWSGIHandler', 'WebSocketWSGIRequestHandler',
38+
'WSGIServer']
39+
40+
logger = logging.getLogger('ws4py')
41+
42+
class WebSocketWSGIHandler(SimpleHandler):
43+
def setup_environ(self):
44+
"""
45+
Setup the environ dictionary and add the
46+
`'ws4py.socket'` key. Its associated value
47+
is the real socket underlying socket.
48+
"""
49+
SimpleHandler.setup_environ(self)
50+
self.environ['ws4py.socket'] = self.environ['wsgi.input']._sock
51+
52+
def finish_response(self):
53+
"""
54+
Completes the response and performs the following tasks:
55+
56+
- Remove the `'ws4py.socket'` and `'ws4py.websocket'`
57+
environ keys.
58+
- Attach the returned websocket, if any, to the WSGI server
59+
using its ``link_websocket_to_server`` method.
60+
"""
61+
ws = None
62+
if self.environ:
63+
self.environ.pop('ws4py.socket', None)
64+
ws = self.environ.pop('ws4y.websocket', None)
65+
66+
try:
67+
SimpleHandler.finish_response(self)
68+
except:
69+
if ws:
70+
ws.close(1011, reason='Something broke')
71+
raise
72+
else:
73+
if ws:
74+
self.request_handler.server.link_websocket_to_server(ws)
75+
76+
class WebSocketWSGIRequestHandler(WSGIRequestHandler):
77+
def handle(self):
78+
"""
79+
Unfortunately the base class forces us
80+
to override the whole method to actually provide our wsgi handler.
81+
"""
82+
self.raw_requestline = self.rfile.readline()
83+
if not self.parse_request(): # An error code has been sent, just exit
84+
return
85+
86+
# next line is where we'd have expect a configuration key somehow
87+
handler = WebSocketWSGIHandler(
88+
self.rfile, self.wfile, self.get_stderr(), self.get_environ()
89+
)
90+
handler.request_handler = self # backpointer for logging
91+
handler.run(self.server.get_app())
92+
93+
class WSGIServer(_WSGIServer):
94+
def initialize_websockets_manager(self):
95+
"""
96+
Call thos to start the underlying websockets
97+
manager. Make sure to call it once your server
98+
is created.
99+
"""
100+
self.manager = WebSocketManager()
101+
self.manager.start()
102+
103+
def shutdown_request(self, request):
104+
"""
105+
The base class would close our socket
106+
if we didn't override it.
107+
"""
108+
pass
109+
110+
def link_websocket_to_server(self, ws):
111+
"""
112+
Call this from your WSGI handler when a websocket
113+
has been created.
114+
"""
115+
self.manager.add(ws)
116+
117+
def server_close(self):
118+
"""
119+
Properly initiate closing handshakes on
120+
all websockets when the WSGI server terminates.
121+
"""
122+
if hasattr(self, 'manager'):
123+
self.manager.close_all()
124+
self.manager.stop()
125+
self.manager.join()
126+
delattr(self, 'manager')
127+
_WSGIServer.server_close(self)
128+
129+
if __name__ == '__main__':
130+
from wsgiref.simple_server import make_server
131+
from ws4py.websocket import EchoWebSocket
132+
133+
server = make_server('', 9000, server_class=WSGIServer,
134+
handler_class=WebSocketWSGIRequestHandler,
135+
app=WebSocketWSGIApplication(handler_cls=EchoWebSocket))
136+
server.initialize_websockets_manager()
137+
try:
138+
server.serve_forever()
139+
except KeyboardInterrupt:
140+
server.server_close()

0 commit comments

Comments
 (0)