├── index.html
├── README.md
├── simple_server.py
├── websocket.py
└── test_websocket.py
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Twisted WebSocket Test
4 |
5 |
25 |
26 |
27 |
28 | WebSocket in Twisted Test
29 | Not Connected
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Twisted WebSocket Server
2 | ========================
3 |
4 | To run:
5 |
6 | $ sudo python simple_server.py
7 |
8 | In your browser go to http://localhost:8080.
9 |
10 | N.B.: Why sudo? It's because the simple server hosts a flash socket policy file on port
11 | 843. This is optional, of course.
12 |
13 | Notes
14 | =====
15 |
16 | This code is based off the associated branch for
17 | http://twistedmatrix.com/trac/ticket/4173, and includes support for the new
18 | `"hixie-76"` handshake (http://www.whatwg.org/specs/web-socket-protocol/) which
19 | is the latest draft as of June 17, 2010.
20 |
21 | The difference between this and what may actually be included in Twisted is
22 | that this version contains definite backwards-support for `hixie-75`, and will
23 | track the latest standard as fast as I can implement it. That means that this
24 | server should work with Chrome 5, Safari 5 and the latest development version
25 | of Chrome 6.
26 |
27 | If using browsers that do *not* support WebSockets, consider using a fallabck
28 | implementation to Flash, as seen in http://github.com/gimite/web-socket-js. The
29 | bundled `test_server.py` will also start a simple Flash Socket Policy server
30 | (see http://www.adobe.com/devnet/flashplayer/articles/socket_policy_files.html)
31 | and should be immediately usable with `web-socket-js`.
32 |
33 |
--------------------------------------------------------------------------------
/simple_server.py:
--------------------------------------------------------------------------------
1 | """ WebSocket test resource.
2 |
3 | This code will run a websocket resource on 8080 and reachable at ws://localhost:8080/test.
4 | For compatibility with web-socket-js (a fallback to Flash for browsers that do not yet support
5 | WebSockets) a policy server will also start on port 843.
6 | See: http://github.com/gimite/web-socket-js
7 | """
8 |
9 | __author__ = 'Reza Lotun'
10 |
11 |
12 | from datetime import datetime
13 |
14 | from twisted.internet.protocol import Protocol, Factory
15 | from twisted.web import resource
16 | from twisted.web.static import File
17 | from twisted.internet import task
18 |
19 | from websocket import WebSocketHandler, WebSocketSite
20 |
21 |
22 | class Testhandler(WebSocketHandler):
23 | def __init__(self, transport):
24 | WebSocketHandler.__init__(self, transport)
25 | self.periodic_call = task.LoopingCall(self.send_time)
26 |
27 | def __del__(self):
28 | print 'Deleting handler'
29 |
30 | def send_time(self):
31 | # send current time as an ISO8601 string
32 | data = datetime.utcnow().isoformat().encode('utf8')
33 | self.transport.write(data)
34 |
35 | def frameReceived(self, frame):
36 | print 'Peer: ', self.transport.getPeer()
37 | self.transport.write(frame)
38 | self.periodic_call.start(0.5)
39 |
40 | def connectionMade(self):
41 | print 'Connected to client.'
42 | # here would be a good place to register this specific handler
43 | # in a dictionary mapping some client identifier (like IPs) against
44 | # self (this handler object)
45 |
46 | def connectionLost(self, reason):
47 | print 'Lost connection.'
48 | self.periodic_call.stop()
49 | del self.periodic_call
50 | # here is a good place to deregister this handler object
51 |
52 |
53 | class FlashSocketPolicy(Protocol):
54 | """ A simple Flash socket policy server.
55 | See: http://www.adobe.com/devnet/flashplayer/articles/socket_policy_files.html
56 | """
57 | def connectionMade(self):
58 | policy = '' \
60 | ''
61 | self.transport.write(policy)
62 | self.transport.loseConnection()
63 |
64 |
65 |
66 | if __name__ == "__main__":
67 | from twisted.internet import reactor
68 |
69 | # run our websocket server
70 | # serve index.html from the local directory
71 | root = File('.')
72 | site = WebSocketSite(root)
73 | site.addHandler('/test', Testhandler)
74 | reactor.listenTCP(8080, site)
75 | # run policy file server
76 | factory = Factory()
77 | factory.protocol = FlashSocketPolicy
78 | reactor.listenTCP(843, factory)
79 | reactor.run()
80 |
81 |
--------------------------------------------------------------------------------
/websocket.py:
--------------------------------------------------------------------------------
1 | # -*- test-case-name: twisted.web.test.test_websocket -*-
2 | # Copyright (c) 2009 Twisted Matrix Laboratories.
3 | # See LICENSE for details.
4 |
5 | """
6 | Note: This is from the associated branch for http://twistedmatrix.com/trac/ticket/4173
7 | and includes support for the hixie-76 handshake.
8 |
9 | WebSocket server protocol.
10 |
11 | See U{http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol} for the
12 | current version of the specification.
13 |
14 | @since: 10.1
15 | """
16 |
17 | from hashlib import md5
18 | import struct
19 |
20 | from twisted.internet import interfaces
21 | from twisted.web.http import datetimeToString
22 | from twisted.web.http import _IdentityTransferDecoder
23 | from twisted.web.server import Request, Site, version, unquote
24 | from zope.interface import implements
25 |
26 |
27 | _ascii_numbers = frozenset(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'])
28 |
29 | class WebSocketRequest(Request):
30 | """
31 | A general purpose L{Request} supporting connection upgrade for WebSocket.
32 | """
33 |
34 | def process(self):
35 | if (self.requestHeaders.getRawHeaders("Upgrade") == ["WebSocket"] and
36 | self.requestHeaders.getRawHeaders("Connection") == ["Upgrade"]):
37 | return self.processWebSocket()
38 | else:
39 | return Request.process(self)
40 |
41 | def processWebSocket(self):
42 | """
43 | Process a specific web socket request.
44 | """
45 | # get site from channel
46 | self.site = self.channel.site
47 |
48 | # set various default headers
49 | self.setHeader("server", version)
50 | self.setHeader("date", datetimeToString())
51 |
52 | # Resource Identification
53 | self.prepath = []
54 | self.postpath = map(unquote, self.path[1:].split("/"))
55 | self.renderWebSocket()
56 |
57 |
58 | def _clientHandshake76(self):
59 | """
60 | Complete hixie-76 handshake, which consists of a challenge and response.
61 |
62 | If the request is not identified with a proper WebSocket handshake, the
63 | connection will be closed. Otherwise, the response to the handshake is
64 | sent and a C{WebSocketHandler} is created to handle the request.
65 | """
66 | def finish():
67 | self.channel.transport.loseConnection()
68 | if self.queued:
69 | return finish()
70 |
71 | secKey1 = self.requestHeaders.getRawHeaders("Sec-WebSocket-Key1", [])
72 | secKey2 = self.requestHeaders.getRawHeaders("Sec-WebSocket-Key2", [])
73 |
74 | if len(secKey1) != 1 or len(secKey2) != 1:
75 | return finish()
76 |
77 | # copied
78 | originHeaders = self.requestHeaders.getRawHeaders("Origin", [])
79 | if len(originHeaders) != 1:
80 | return finish()
81 | hostHeaders = self.requestHeaders.getRawHeaders("Host", [])
82 | if len(hostHeaders) != 1:
83 | return finish()
84 | handlerFactory = self.site.handlers.get(self.uri)
85 | if not handlerFactory:
86 | return finish()
87 |
88 | # key1 and key2 exist and are a string of characters
89 | # filter both keys to get a string with all numbers in order
90 | key1 = secKey1[0]
91 | key2 = secKey2[0]
92 | numBuffer1 = ''.join([x for x in key1 if x in _ascii_numbers])
93 | numBuffer2 = ''.join([x for x in key2 if x in _ascii_numbers])
94 |
95 | # make sure numbers actually exist
96 | if not numBuffer1 or not numBuffer2:
97 | return finish()
98 |
99 | # these should be int-like
100 | num1 = int(numBuffer1)
101 | num2 = int(numBuffer2)
102 |
103 | # count the number of spaces in each character string
104 | numSpaces1 = 0
105 | for x in key1:
106 | if x == ' ':
107 | numSpaces1 += 1
108 | numSpaces2 = 0
109 | for x in key2:
110 | if x == ' ':
111 | numSpaces2 += 1
112 |
113 | # there should be at least one space in each
114 | if numSpaces1 == 0 or numSpaces2 == 0:
115 | return finish()
116 |
117 | # get two resulting numbers, as specified in hixie-76
118 | num1 = num1 / numSpaces1
119 | num2 = num2 / numSpaces2
120 |
121 | transport = WebSocketTransport(self)
122 | handler = handlerFactory(transport)
123 | transport._attachHandler(handler)
124 |
125 | self.channel.setRawMode()
126 |
127 | def finishHandshake(nonce):
128 | """ Receive nonce value from request body, and calculate repsonse. """
129 | protocolHeaders = self.requestHeaders.getRawHeaders(
130 | "WebSocket-Protocol", [])
131 | if len(protocolHeaders) not in (0, 1):
132 | return finish()
133 | if protocolHeaders:
134 | if protocolHeaders[0] not in self.site.supportedProtocols:
135 | return finish()
136 | protocolHeader = protocolHeaders[0]
137 | else:
138 | protocolHeader = None
139 |
140 | originHeader = originHeaders[0]
141 | hostHeader = hostHeaders[0]
142 | self.startedWriting = True
143 | handshake = [
144 | "HTTP/1.1 101 Web Socket Protocol Handshake",
145 | "Upgrade: WebSocket",
146 | "Connection: Upgrade"]
147 | handshake.append("Sec-WebSocket-Origin: %s" % (originHeader))
148 | if self.isSecure():
149 | scheme = "wss"
150 | else:
151 | scheme = "ws"
152 | handshake.append(
153 | "Sec-WebSocket-Location: %s://%s%s" % (
154 | scheme, hostHeader, self.uri))
155 |
156 | if protocolHeader is not None:
157 | handshake.append("Sec-WebSocket-Protocol: %s" % protocolHeader)
158 |
159 | for header in handshake:
160 | self.write("%s\r\n" % header)
161 |
162 | self.write("\r\n")
163 |
164 | # concatenate num1 (32 bit in), num2 (32 bit int), nonce, and take md5 of result
165 | res = struct.pack('>II8s', num1, num2, nonce)
166 | server_response = md5(res).digest()
167 | self.write(server_response)
168 |
169 | # XXX we probably don't want to set _transferDecoder
170 | self.channel._transferDecoder = WebSocketFrameDecoder(
171 | self, handler)
172 |
173 | transport._connectionMade()
174 |
175 | # we need the nonce from the request body
176 | self.channel._transferDecoder = _IdentityTransferDecoder(0, lambda _ : None, finishHandshake)
177 |
178 |
179 | def _checkClientHandshake(self):
180 | """
181 | Verify client handshake, closing the connection in case of problem.
182 |
183 | @return: C{None} if a problem was detected, or a tuple of I{Origin}
184 | header, I{Host} header, I{WebSocket-Protocol} header, and
185 | C{WebSocketHandler} instance. The I{WebSocket-Protocol} header will
186 | be C{None} if not specified by the client.
187 | """
188 | def finish():
189 | self.channel.transport.loseConnection()
190 | if self.queued:
191 | return finish()
192 | originHeaders = self.requestHeaders.getRawHeaders("Origin", [])
193 | if len(originHeaders) != 1:
194 | return finish()
195 | hostHeaders = self.requestHeaders.getRawHeaders("Host", [])
196 | if len(hostHeaders) != 1:
197 | return finish()
198 |
199 | handlerFactory = self.site.handlers.get(self.uri)
200 | if not handlerFactory:
201 | return finish()
202 | transport = WebSocketTransport(self)
203 | handler = handlerFactory(transport)
204 | transport._attachHandler(handler)
205 |
206 | protocolHeaders = self.requestHeaders.getRawHeaders(
207 | "WebSocket-Protocol", [])
208 | if len(protocolHeaders) not in (0, 1):
209 | return finish()
210 | if protocolHeaders:
211 | if protocolHeaders[0] not in self.site.supportedProtocols:
212 | return finish()
213 | protocolHeader = protocolHeaders[0]
214 | else:
215 | protocolHeader = None
216 | return originHeaders[0], hostHeaders[0], protocolHeader, handler
217 |
218 |
219 | def renderWebSocket(self):
220 | """
221 | Render a WebSocket request.
222 |
223 | If the request is not identified with a proper WebSocket handshake, the
224 | connection will be closed. Otherwise, the response to the handshake is
225 | sent and a C{WebSocketHandler} is created to handle the request.
226 | """
227 | # check for post-75 handshake requests
228 | isSecHandshake = self.requestHeaders.getRawHeaders("Sec-WebSocket-Key1", [])
229 | if isSecHandshake:
230 | self._clientHandshake76()
231 | else:
232 | check = self._checkClientHandshake()
233 | if check is None:
234 | return
235 | originHeader, hostHeader, protocolHeader, handler = check
236 | self.startedWriting = True
237 | handshake = [
238 | "HTTP/1.1 101 Web Socket Protocol Handshake",
239 | "Upgrade: WebSocket",
240 | "Connection: Upgrade"]
241 | handshake.append("WebSocket-Origin: %s" % (originHeader))
242 | if self.isSecure():
243 | scheme = "wss"
244 | else:
245 | scheme = "ws"
246 | handshake.append(
247 | "WebSocket-Location: %s://%s%s" % (
248 | scheme, hostHeader, self.uri))
249 |
250 | if protocolHeader is not None:
251 | handshake.append("WebSocket-Protocol: %s" % protocolHeader)
252 |
253 | for header in handshake:
254 | self.write("%s\r\n" % header)
255 |
256 | self.write("\r\n")
257 | self.channel.setRawMode()
258 | # XXX we probably don't want to set _transferDecoder
259 | self.channel._transferDecoder = WebSocketFrameDecoder(
260 | self, handler)
261 | handler.transport._connectionMade()
262 | return
263 |
264 |
265 |
266 | class WebSocketSite(Site):
267 | """
268 | @ivar handlers: a C{dict} of names to L{WebSocketHandler} factories.
269 | @type handlers: C{dict}
270 | @ivar supportedProtocols: a C{list} of supported I{WebSocket-Protocol}
271 | values. If a value is passed at handshake and doesn't figure in this
272 | list, the connection is closed.
273 | @type supportedProtocols: C{list}
274 | """
275 | requestFactory = WebSocketRequest
276 |
277 | def __init__(self, resource, logPath=None, timeout=60*60*12,
278 | supportedProtocols=None):
279 | Site.__init__(self, resource, logPath, timeout)
280 | self.handlers = {}
281 | self.supportedProtocols = supportedProtocols or []
282 |
283 | def addHandler(self, name, handlerFactory):
284 | """
285 | Add or override a handler for the given C{name}.
286 |
287 | @param name: the resource name to be handled.
288 | @type name: C{str}
289 | @param handlerFactory: a C{WebSocketHandler} factory.
290 | @type handlerFactory: C{callable}
291 | """
292 | if not name.startswith("/"):
293 | raise ValueError("Invalid resource name.")
294 | self.handlers[name] = handlerFactory
295 |
296 |
297 |
298 | class WebSocketTransport(object):
299 | """
300 | Transport abstraction over WebSocket, providing classic Twisted methods and
301 | callbacks.
302 | """
303 | implements(interfaces.ITransport)
304 |
305 | _handler = None
306 |
307 | def __init__(self, request):
308 | self._request = request
309 | self._request.notifyFinish().addErrback(self._connectionLost)
310 |
311 | def _attachHandler(self, handler):
312 | """
313 | Attach the given L{WebSocketHandler} to this transport.
314 | """
315 | self._handler = handler
316 |
317 | def _connectionMade(self):
318 | """
319 | Called when a connection is made.
320 | """
321 | self._handler.connectionMade()
322 |
323 | def _connectionLost(self, reason):
324 | """
325 | Forward connection lost event to the L{WebSocketHandler}.
326 | """
327 | self._handler.connectionLost(reason)
328 | del self._request.transport
329 | del self._request
330 | del self._handler
331 |
332 | def getPeer(self):
333 | """
334 | Return a tuple describing the other side of the connection.
335 |
336 | @rtype: C{tuple}
337 | """
338 | return self._request.transport.getPeer()
339 |
340 | def getHost(self):
341 | """
342 | Similar to getPeer, but returns an address describing this side of the
343 | connection.
344 |
345 | @return: An L{IAddress} provider.
346 | """
347 |
348 | return self._request.transport.getHost()
349 |
350 | def write(self, frame):
351 | """
352 | Send the given frame to the connected client.
353 |
354 | @param frame: a I{UTF-8} encoded C{str} to send to the client.
355 | @type frame: C{str}
356 | """
357 | self._request.write("\x00%s\xff" % frame)
358 |
359 | def writeSequence(self, frames):
360 | """
361 | Send a sequence of frames to the connected client.
362 | """
363 | self._request.write("".join(["\x00%s\xff" % f for f in frames]))
364 |
365 | def loseConnection(self):
366 | """
367 | Close the connection.
368 | """
369 | self._request.transport.loseConnection()
370 | del self._request.transport
371 | del self._request
372 | del self._handler
373 |
374 | class WebSocketHandler(object):
375 | """
376 | Base class for handling WebSocket connections. It mainly provides a
377 | transport to send frames, and a callback called when frame are received,
378 | C{frameReceived}.
379 |
380 | @ivar transport: a C{WebSocketTransport} instance.
381 | @type: L{WebSocketTransport}
382 | """
383 |
384 | def __init__(self, transport):
385 | """
386 | Create the handler, with the given transport
387 | """
388 | self.transport = transport
389 |
390 |
391 | def frameReceived(self, frame):
392 | """
393 | Called when a frame is received.
394 |
395 | @param frame: a I{UTF-8} encoded C{str} sent by the client.
396 | @type frame: C{str}
397 | """
398 |
399 |
400 | def frameLengthExceeded(self):
401 | """
402 | Called when too big a frame is received. The default behavior is to
403 | close the connection, but it can be customized to do something else.
404 | """
405 | self.transport.loseConnection()
406 |
407 |
408 | def connectionMade(self):
409 | """
410 | Called when a connection is made.
411 | """
412 |
413 | def connectionLost(self, reason):
414 | """
415 | Callback called when the underlying transport has detected that the
416 | connection is closed.
417 | """
418 |
419 |
420 |
421 | class WebSocketFrameDecoder(object):
422 | """
423 | Decode WebSocket frames and pass them to the attached C{WebSocketHandler}
424 | instance.
425 |
426 | @ivar MAX_LENGTH: maximum len of the frame allowed, before calling
427 | C{frameLengthExceeded} on the handler.
428 | @type MAX_LENGTH: C{int}
429 | @ivar request: C{Request} instance.
430 | @type request: L{twisted.web.server.Request}
431 | @ivar handler: L{WebSocketHandler} instance handling the request.
432 | @type handler: L{WebSocketHandler}
433 | @ivar _data: C{list} of C{str} buffering the received data.
434 | @type _data: C{list} of C{str}
435 | @ivar _currentFrameLength: length of the current handled frame, plus the
436 | additional leading byte.
437 | @type _currentFrameLength: C{int}
438 | """
439 |
440 | MAX_LENGTH = 16384
441 |
442 |
443 | def __init__(self, request, handler):
444 | self.request = request
445 | self.handler = handler
446 | self._data = []
447 | self._currentFrameLength = 0
448 |
449 | def dataReceived(self, data):
450 | """
451 | Parse data to read WebSocket frames.
452 |
453 | @param data: data received over the WebSocket connection.
454 | @type data: C{str}
455 | """
456 | if not data:
457 | return
458 | while True:
459 | endIndex = data.find("\xff")
460 | if endIndex != -1:
461 | self._currentFrameLength += endIndex
462 | if self._currentFrameLength > self.MAX_LENGTH:
463 | self.handler.frameLengthExceeded()
464 | break
465 | self._currentFrameLength = 0
466 | frame = "".join(self._data) + data[:endIndex]
467 | self._data[:] = []
468 | if frame[0] != "\x00":
469 | self.request.transport.loseConnection()
470 | break
471 | self.handler.frameReceived(frame[1:])
472 | data = data[endIndex + 1:]
473 | if not data:
474 | break
475 | if data[0] != "\x00":
476 | self.request.transport.loseConnection()
477 | break
478 | else:
479 | self._currentFrameLength += len(data)
480 | if self._currentFrameLength > self.MAX_LENGTH + 1:
481 | self.handler.frameLengthExceeded()
482 | else:
483 | self._data.append(data)
484 | break
485 |
486 |
487 |
488 | __all__ = ["WebSocketHandler", "WebSocketSite"]
489 |
490 |
--------------------------------------------------------------------------------
/test_websocket.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2009 Twisted Matrix Laboratories.
2 | # See LICENSE for details.
3 |
4 | """
5 | Tests for L{twisted.web.websocket}.
6 | """
7 |
8 | from twisted.internet.main import CONNECTION_DONE
9 | from twisted.internet.error import ConnectionDone
10 | from twisted.python.failure import Failure
11 |
12 | from websocket import WebSocketHandler, WebSocketFrameDecoder
13 | from websocket import WebSocketSite, WebSocketTransport
14 |
15 | from twisted.web.resource import Resource
16 | from twisted.web.server import Request, Site
17 | from twisted.web.test.test_web import DummyChannel
18 | from twisted.trial.unittest import TestCase
19 |
20 |
21 |
22 | class DummyChannel(DummyChannel):
23 | """
24 | A L{DummyChannel} supporting the C{setRawMode} method.
25 |
26 | @ivar raw: C{bool} indicating if C{setRawMode} has been called.
27 | """
28 |
29 | raw = False
30 |
31 | def setRawMode(self):
32 | self.raw = True
33 |
34 |
35 |
36 | class TestHandler(WebSocketHandler):
37 | """
38 | A L{WebSocketHandler} recording every frame received.
39 |
40 | @ivar frames: C{list} of frames received.
41 | @ivar lostReason: reason for connection closing.
42 | """
43 |
44 | def __init__(self, request):
45 | WebSocketHandler.__init__(self, request)
46 | self.frames = []
47 | self.lostReason = None
48 |
49 |
50 | def frameReceived(self, frame):
51 | self.frames.append(frame)
52 |
53 |
54 | def connectionLost(self, reason):
55 | self.lostReason = reason
56 |
57 |
58 |
59 | class WebSocketSiteTestCase(TestCase):
60 | """
61 | Tests for L{WebSocketSite}.
62 | """
63 |
64 | def setUp(self):
65 | self.site = WebSocketSite(Resource())
66 | self.site.addHandler("/test", TestHandler)
67 |
68 |
69 | def renderRequest(self, headers=None, url="/test", ssl=False,
70 | queued=False, body=None):
71 | """
72 | Render a request against C{self.site}, writing the WebSocket
73 | handshake.
74 | """
75 | if headers is None:
76 | headers = [
77 | ("Upgrade", "WebSocket"), ("Connection", "Upgrade"),
78 | ("Host", "localhost"), ("Origin", "http://localhost/")]
79 | channel = DummyChannel()
80 | if ssl:
81 | channel.transport = channel.SSL()
82 | channel.site = self.site
83 | request = self.site.requestFactory(channel, queued)
84 | for k, v in headers:
85 | request.requestHeaders.addRawHeader(k, v)
86 | request.gotLength(0)
87 | request.requestReceived("GET", url, "HTTP/1.1")
88 | if body:
89 | request.channel._transferDecoder.finishCallback(body)
90 | return channel
91 |
92 |
93 | def test_multiplePostpath(self):
94 | """
95 | A resource name can consist of several path elements.
96 | """
97 | handlers = []
98 | def handlerFactory(request):
99 | handler = TestHandler(request)
100 | handlers.append(handler)
101 | return handler
102 | self.site.addHandler("/foo/bar", handlerFactory)
103 | channel = self.renderRequest(url="/foo/bar")
104 | self.assertEquals(len(handlers), 1)
105 | self.assertFalse(channel.transport.disconnected)
106 |
107 |
108 | def test_queryArguments(self):
109 | """
110 | A resource name may contain query arguments.
111 | """
112 | handlers = []
113 | def handlerFactory(request):
114 | handler = TestHandler(request)
115 | handlers.append(handler)
116 | return handler
117 | self.site.addHandler("/test?foo=bar&egg=spam", handlerFactory)
118 | channel = self.renderRequest(url="/test?foo=bar&egg=spam")
119 | self.assertEquals(len(handlers), 1)
120 | self.assertFalse(channel.transport.disconnected)
121 |
122 |
123 | def test_noOriginHeader(self):
124 | """
125 | If no I{Origin} header is present, the connection is closed.
126 | """
127 | channel = self.renderRequest(
128 | headers=[("Upgrade", "WebSocket"), ("Connection", "Upgrade"),
129 | ("Host", "localhost")])
130 | self.assertFalse(channel.transport.written.getvalue())
131 | self.assertTrue(channel.transport.disconnected)
132 |
133 |
134 | def test_multipleOriginHeaders(self):
135 | """
136 | If more than one I{Origin} header is present, the connection is
137 | dropped.
138 | """
139 | channel = self.renderRequest(
140 | headers=[("Upgrade", "WebSocket"), ("Connection", "Upgrade"),
141 | ("Host", "localhost"), ("Origin", "foo"),
142 | ("Origin", "bar")])
143 | self.assertFalse(channel.transport.written.getvalue())
144 | self.assertTrue(channel.transport.disconnected)
145 |
146 |
147 | def test_noHostHeader(self):
148 | """
149 | If no I{Host} header is present, the connection is dropped.
150 | """
151 | channel = self.renderRequest(
152 | headers=[("Upgrade", "WebSocket"), ("Connection", "Upgrade"),
153 | ("Origin", "http://localhost/")])
154 | self.assertFalse(channel.transport.written.getvalue())
155 | self.assertTrue(channel.transport.disconnected)
156 |
157 |
158 | def test_multipleHostHeaders(self):
159 | """
160 | If more than one I{Host} header is present, the connection is
161 | dropped.
162 | """
163 | channel = self.renderRequest(
164 | headers=[("Upgrade", "WebSocket"), ("Connection", "Upgrade"),
165 | ("Origin", "http://localhost/"), ("Host", "foo"),
166 | ("Host", "bar")])
167 | self.assertFalse(channel.transport.written.getvalue())
168 | self.assertTrue(channel.transport.disconnected)
169 |
170 |
171 | def test_missingHandler(self):
172 | """
173 | If no handler is registered for the given resource, the connection is
174 | dropped.
175 | """
176 | channel = self.renderRequest(url="/foo")
177 | self.assertFalse(channel.transport.written.getvalue())
178 | self.assertTrue(channel.transport.disconnected)
179 |
180 |
181 | def test_noConnectionUpgrade(self):
182 | """
183 | If the I{Connection: Upgrade} header is not present, the connection is
184 | dropped.
185 | """
186 | channel = self.renderRequest(
187 | headers=[("Upgrade", "WebSocket"), ("Host", "localhost"),
188 | ("Origin", "http://localhost/")])
189 | self.assertIn("404 Not Found", channel.transport.written.getvalue())
190 |
191 |
192 | def test_noUpgradeWebSocket(self):
193 | """
194 | If the I{Upgrade: WebSocket} header is not present, the connection is
195 | dropped.
196 | """
197 | channel = self.renderRequest(
198 | headers=[("Connection", "Upgrade"), ("Host", "localhost"),
199 | ("Origin", "http://localhost/")])
200 | self.assertIn("404 Not Found", channel.transport.written.getvalue())
201 |
202 |
203 | def test_render(self):
204 | """
205 | If the handshake is successful, we can read back the server handshake,
206 | and the channel is setup for raw mode.
207 | """
208 | channel = self.renderRequest()
209 | self.assertTrue(channel.raw)
210 | self.assertEquals(
211 | channel.transport.written.getvalue(),
212 | "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
213 | "Upgrade: WebSocket\r\n"
214 | "Connection: Upgrade\r\n"
215 | "WebSocket-Origin: http://localhost/\r\n"
216 | "WebSocket-Location: ws://localhost/test\r\n\r\n")
217 | self.assertFalse(channel.transport.disconnected)
218 |
219 | def test_render_handShake76(self):
220 | """
221 | Test a hixie-76 handShake.
222 | """
223 | # we need to construct a challenge
224 | key1 = '1x0x0 0y00 0' # 1000000
225 | key2 = '1b0b0 000 0' # 1000000
226 | body = '12345678'
227 | headers = [
228 | ("Upgrade", "WebSocket"), ("Connection", "Upgrade"),
229 | ("Host", "localhost"), ("Origin", "http://localhost/"),
230 | ("Sec-WebSocket-Key1", key1), ("Sec-WebSocket-Key2", key2)]
231 | channel = self.renderRequest(headers=headers, body=body)
232 |
233 | self.assertTrue(channel.raw)
234 |
235 | result = channel.transport.written.getvalue()
236 |
237 | headers, response = result.split('\r\n\r\n')
238 |
239 | self.assertEquals(
240 | headers,
241 | "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
242 | "Upgrade: WebSocket\r\n"
243 | "Connection: Upgrade\r\n"
244 | "Sec-WebSocket-Origin: http://localhost/\r\n"
245 | "Sec-WebSocket-Location: ws://localhost/test")
246 |
247 | # check challenge is correct
248 | from hashlib import md5
249 | import struct
250 | self.assertEquals(md5(struct.pack('>ii8s', 500000, 500000, body)).digest(), response)
251 |
252 | self.assertFalse(channel.transport.disconnected)
253 |
254 | def test_secureRender(self):
255 | """
256 | If the WebSocket connection is over SSL, the I{WebSocket-Location}
257 | header specified I{wss} as scheme.
258 | """
259 | channel = self.renderRequest(ssl=True)
260 | self.assertTrue(channel.raw)
261 | self.assertEquals(
262 | channel.transport.written.getvalue(),
263 | "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
264 | "Upgrade: WebSocket\r\n"
265 | "Connection: Upgrade\r\n"
266 | "WebSocket-Origin: http://localhost/\r\n"
267 | "WebSocket-Location: wss://localhost/test\r\n\r\n")
268 | self.assertFalse(channel.transport.disconnected)
269 |
270 |
271 | def test_frameReceived(self):
272 | """
273 | C{frameReceived} is called with the received frames after handshake.
274 | """
275 | handlers = []
276 | def handlerFactory(request):
277 | handler = TestHandler(request)
278 | handlers.append(handler)
279 | return handler
280 | self.site.addHandler("/test2", handlerFactory)
281 | channel = self.renderRequest(url="/test2")
282 | self.assertEquals(len(handlers), 1)
283 | handler = handlers[0]
284 | channel._transferDecoder.dataReceived("\x00hello\xff\x00boy\xff")
285 | self.assertEquals(handler.frames, ["hello", "boy"])
286 |
287 |
288 | def test_websocketProtocolAccepted(self):
289 | """
290 | The I{WebSocket-Protocol} header is echoed by the server if the
291 | protocol is among the supported protocols.
292 | """
293 | self.site.supportedProtocols.append("pixiedust")
294 | channel = self.renderRequest(
295 | headers = [
296 | ("Upgrade", "WebSocket"), ("Connection", "Upgrade"),
297 | ("Host", "localhost"), ("Origin", "http://localhost/"),
298 | ("WebSocket-Protocol", "pixiedust")])
299 | self.assertTrue(channel.raw)
300 | self.assertEquals(
301 | channel.transport.written.getvalue(),
302 | "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
303 | "Upgrade: WebSocket\r\n"
304 | "Connection: Upgrade\r\n"
305 | "WebSocket-Origin: http://localhost/\r\n"
306 | "WebSocket-Location: ws://localhost/test\r\n"
307 | "WebSocket-Protocol: pixiedust\r\n\r\n")
308 | self.assertFalse(channel.transport.disconnected)
309 |
310 |
311 | def test_tooManyWebSocketProtocol(self):
312 | """
313 | If more than one I{WebSocket-Protocol} headers are specified, the
314 | connection is dropped.
315 | """
316 | self.site.supportedProtocols.append("pixiedust")
317 | channel = self.renderRequest(
318 | headers = [
319 | ("Upgrade", "WebSocket"), ("Connection", "Upgrade"),
320 | ("Host", "localhost"), ("Origin", "http://localhost/"),
321 | ("WebSocket-Protocol", "pixiedust"),
322 | ("WebSocket-Protocol", "fairymagic")])
323 | self.assertFalse(channel.transport.written.getvalue())
324 | self.assertTrue(channel.transport.disconnected)
325 |
326 |
327 | def test_unsupportedProtocols(self):
328 | """
329 | If the I{WebSocket-Protocol} header specified an unsupported protocol,
330 | the connection is dropped.
331 | """
332 | self.site.supportedProtocols.append("pixiedust")
333 | channel = self.renderRequest(
334 | headers = [
335 | ("Upgrade", "WebSocket"), ("Connection", "Upgrade"),
336 | ("Host", "localhost"), ("Origin", "http://localhost/"),
337 | ("WebSocket-Protocol", "fairymagic")])
338 | self.assertFalse(channel.transport.written.getvalue())
339 | self.assertTrue(channel.transport.disconnected)
340 |
341 |
342 | def test_queued(self):
343 | """
344 | Queued requests are unsupported, thus closed by the
345 | C{WebSocketSite}.
346 | """
347 | channel = self.renderRequest(queued=True)
348 | self.assertFalse(channel.transport.written.getvalue())
349 | self.assertTrue(channel.transport.disconnected)
350 |
351 |
352 | def test_addHandlerWithoutSlash(self):
353 | """
354 | C{addHandler} raises C{ValueError} if the resource name doesn't start
355 | with a slash.
356 | """
357 | self.assertRaises(
358 | ValueError, self.site.addHandler, "test", TestHandler)
359 |
360 |
361 |
362 | class WebSocketFrameDecoderTestCase(TestCase):
363 | """
364 | Test for C{WebSocketFrameDecoder}.
365 | """
366 |
367 | def setUp(self):
368 | self.channel = DummyChannel()
369 | request = Request(self.channel, False)
370 | transport = WebSocketTransport(request)
371 | handler = TestHandler(transport)
372 | transport._attachHandler(handler)
373 | self.decoder = WebSocketFrameDecoder(request, handler)
374 | self.decoder.MAX_LENGTH = 100
375 |
376 |
377 | def test_oneFrame(self):
378 | """
379 | We can send one frame handled with one C{dataReceived} call.
380 | """
381 | self.decoder.dataReceived("\x00frame\xff")
382 | self.assertEquals(self.decoder.handler.frames, ["frame"])
383 |
384 |
385 | def test_oneFrameSplitted(self):
386 | """
387 | A frame can be split into several C{dataReceived} calls, and will be
388 | combined again when sent to the C{WebSocketHandler}.
389 | """
390 | self.decoder.dataReceived("\x00fra")
391 | self.decoder.dataReceived("me\xff")
392 | self.assertEquals(self.decoder.handler.frames, ["frame"])
393 |
394 |
395 | def test_multipleFrames(self):
396 | """
397 | Several frames can be received in a single C{dataReceived} call.
398 | """
399 | self.decoder.dataReceived("\x00frame1\xff\x00frame2\xff")
400 | self.assertEquals(self.decoder.handler.frames, ["frame1", "frame2"])
401 |
402 |
403 | def test_missingNull(self):
404 | """
405 | If a frame not starting with C{\\x00} is received, the connection is
406 | dropped.
407 | """
408 | self.decoder.dataReceived("frame\xff")
409 | self.assertTrue(self.channel.transport.disconnected)
410 |
411 |
412 | def test_missingNullAfterGoodFrame(self):
413 | """
414 | If a frame not starting with C{\\x00} is received after a correct
415 | frame, the connection is dropped.
416 | """
417 | self.decoder.dataReceived("\x00frame\xfffoo")
418 | self.assertTrue(self.channel.transport.disconnected)
419 | self.assertEquals(self.decoder.handler.frames, ["frame"])
420 |
421 |
422 | def test_emptyReceive(self):
423 | """
424 | Received an empty string doesn't do anything.
425 | """
426 | self.decoder.dataReceived("")
427 | self.assertFalse(self.channel.transport.disconnected)
428 |
429 |
430 | def test_maxLength(self):
431 | """
432 | If a frame is received which is bigger than C{MAX_LENGTH}, the
433 | connection is dropped.
434 | """
435 | self.decoder.dataReceived("\x00" + "x" * 101)
436 | self.assertTrue(self.channel.transport.disconnected)
437 |
438 |
439 | def test_maxLengthFrameCompleted(self):
440 | """
441 | If a too big frame is received in several fragments, the connection is
442 | dropped.
443 | """
444 | self.decoder.dataReceived("\x00" + "x" * 90)
445 | self.decoder.dataReceived("x" * 11 + "\xff")
446 | self.assertTrue(self.channel.transport.disconnected)
447 |
448 |
449 | def test_frameLengthReset(self):
450 | """
451 | The length of frames is reset between frame, thus not creating an error
452 | when the accumulated length exceeds the maximum frame length.
453 | """
454 | for i in range(15):
455 | self.decoder.dataReceived("\x00" + "x" * 10 + "\xff")
456 | self.assertFalse(self.channel.transport.disconnected)
457 |
458 |
459 |
460 | class WebSocketHandlerTestCase(TestCase):
461 | """
462 | Tests for L{WebSocketHandler}.
463 | """
464 |
465 | def setUp(self):
466 | self.channel = DummyChannel()
467 | self.request = request = Request(self.channel, False)
468 | # Simulate request handling
469 | request.startedWriting = True
470 | transport = WebSocketTransport(request)
471 | self.handler = TestHandler(transport)
472 | transport._attachHandler(self.handler)
473 |
474 |
475 | def test_write(self):
476 | """
477 | L{WebSocketTransport.write} adds the required C{\\x00} and C{\\xff}
478 | around sent frames, and write it to the request.
479 | """
480 | self.handler.transport.write("hello")
481 | self.handler.transport.write("world")
482 | self.assertEquals(
483 | self.channel.transport.written.getvalue(),
484 | "\x00hello\xff\x00world\xff")
485 | self.assertFalse(self.channel.transport.disconnected)
486 |
487 |
488 | def test_close(self):
489 | """
490 | L{WebSocketTransport.loseConnection} closes the underlying request.
491 | """
492 | self.handler.transport.loseConnection()
493 | self.assertTrue(self.channel.transport.disconnected)
494 |
495 |
496 | def test_connectionLost(self):
497 | """
498 | L{WebSocketHandler.connectionLost} is called with the reason of the
499 | connection closing when L{Request.connectionLost} is called.
500 | """
501 | self.request.connectionLost(Failure(CONNECTION_DONE))
502 | self.handler.lostReason.trap(ConnectionDone)
503 |
--------------------------------------------------------------------------------