├── 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 | --------------------------------------------------------------------------------