Ticket #4173: rlotun-websocket-76.patch

File rlotun-websocket-76.patch, 10.1 KB (added by rlotun, 5 years ago)

Support for the hixie-76 new handshake, with unit tests.

  • twisted/web/test/test_websocket.py

     
    6767 
    6868 
    6969    def renderRequest(self, headers=None, url="/test", ssl=False, 
    70                       queued=False): 
     70                      queued=False, body=None): 
    7171        """ 
    7272        Render a request against C{self.site}, writing the WebSocket 
    7373        handshake. 
     
    8585            request.requestHeaders.addRawHeader(k, v) 
    8686        request.gotLength(0) 
    8787        request.requestReceived("GET", url, "HTTP/1.1") 
     88        if body: 
     89            request.channel._transferDecoder.finishCallback(body) 
    8890        return channel 
    8991 
    9092 
     
    214216            "WebSocket-Location: ws://localhost/test\r\n\r\n") 
    215217        self.assertFalse(channel.transport.disconnected) 
    216218 
     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) 
    217232 
     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 
    218254    def test_secureRender(self): 
    219255        """ 
    220256        If the WebSocket connection is over SSL, the I{WebSocket-Location} 
  • twisted/web/websocket.py

     
    1111@since: 10.1 
    1212""" 
    1313 
     14from hashlib import md5 
     15import struct 
    1416 
    1517from twisted.web.http import datetimeToString 
     18from twisted.web.http import _IdentityTransferDecoder 
    1619from twisted.web.server import Request, Site, version, unquote 
    1720 
    1821 
     22_ascii_numbers = frozenset(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']) 
    1923 
    2024class WebSocketRequest(Request): 
    2125    """ 
     
    4751        self.renderWebSocket() 
    4852 
    4953 
     54    def _clientHandshake76(self): 
     55        """ 
     56        Complete hixie-76 handshake, which consists of a challenge and response. 
     57 
     58        If the request is not identified with a proper WebSocket handshake, the 
     59        connection will be closed. Otherwise, the response to the handshake is 
     60        sent and a C{WebSocketHandler} is created to handle the request. 
     61        """ 
     62        def finish(): 
     63            self.channel.transport.loseConnection() 
     64        if self.queued: 
     65            return finish() 
     66 
     67        secKey1 = self.requestHeaders.getRawHeaders("Sec-WebSocket-Key1", []) 
     68        secKey2 = self.requestHeaders.getRawHeaders("Sec-WebSocket-Key2", []) 
     69 
     70        if len(secKey1) != 1 or len(secKey2) != 1: 
     71            return finish() 
     72 
     73        # copied 
     74        originHeaders = self.requestHeaders.getRawHeaders("Origin", []) 
     75        if len(originHeaders) != 1: 
     76            return finish() 
     77        hostHeaders = self.requestHeaders.getRawHeaders("Host", []) 
     78        if len(hostHeaders) != 1: 
     79            return finish() 
     80        handlerFactory = self.site.handlers.get(self.uri) 
     81        if not handlerFactory: 
     82            return finish() 
     83        transport = WebSocketTransport(self) 
     84        handler = handlerFactory(transport) 
     85        transport._attachHandler(handler) 
     86 
     87        # key1 and key2 exist and are a string of characters 
     88        # filter both keys to get a string with all numbers in order 
     89        key1 = secKey1[0] 
     90        key2 = secKey2[0] 
     91        numBuffer1 = ''.join([x for x in key1 if x in _ascii_numbers]) 
     92        numBuffer2 = ''.join([x for x in key2 if x in _ascii_numbers]) 
     93 
     94        # make sure numbers actually exist 
     95        if not numBuffer1 or not numBuffer2: 
     96            return finish() 
     97 
     98        # these should be int-like 
     99        num1 = int(numBuffer1) 
     100        num2 = int(numBuffer2) 
     101 
     102        # count the number of spaces in each character string 
     103        numSpaces1 = 0 
     104        for x in key1: 
     105            if x == ' ': 
     106                numSpaces1 += 1 
     107        numSpaces2 = 0 
     108        for x in key2: 
     109            if x == ' ': 
     110                numSpaces2 += 1 
     111 
     112        # there should be at least one space in each 
     113        if numSpaces1 == 0 or numSpaces2 == 0: 
     114            return finish() 
     115 
     116        # get two resulting numbers, as specified in hixie-76 
     117        num1 = num1 / numSpaces1 
     118        num2 = num2 / numSpaces2 
     119 
     120        self.channel.setRawMode() 
     121 
     122        def finishHandshake(nonce): 
     123            """ Receive nonce value from request body, and calculate repsonse. """ 
     124            protocolHeaders = self.requestHeaders.getRawHeaders( 
     125                "WebSocket-Protocol", []) 
     126            if len(protocolHeaders) not in (0,  1): 
     127                return finish() 
     128            if protocolHeaders: 
     129                if protocolHeaders[0] not in self.site.supportedProtocols: 
     130                    return finish() 
     131                protocolHeader = protocolHeaders[0] 
     132            else: 
     133                protocolHeader = None 
     134 
     135            handler = handlerFactory(transport) 
     136            check = originHeaders[0], hostHeaders[0], protocolHeader, handler 
     137 
     138            originHeader, hostHeader, protocolHeader, handler = check 
     139            self.startedWriting = True 
     140            handshake = [ 
     141                "HTTP/1.1 101 Web Socket Protocol Handshake", 
     142                "Upgrade: WebSocket", 
     143                "Connection: Upgrade"] 
     144            handshake.append("Sec-WebSocket-Origin: %s" % (originHeader)) 
     145            if self.isSecure(): 
     146                scheme = "wss" 
     147            else: 
     148                scheme = "ws" 
     149            handshake.append( 
     150                "Sec-WebSocket-Location: %s://%s%s" % ( 
     151                scheme, hostHeader, self.uri)) 
     152 
     153            if protocolHeader is not None: 
     154                handshake.append("Sec-WebSocket-Protocol: %s" % protocolHeader) 
     155 
     156            for header in handshake: 
     157                self.write("%s\r\n" % header) 
     158 
     159            self.write("\r\n") 
     160 
     161            # concatenate num1 (32 bit in), num2 (32 bit int), nonce, and take md5 of result 
     162            res = struct.pack('>ii8s', num1, num2, nonce) 
     163            server_response = md5(res).digest() 
     164            self.write(server_response) 
     165 
     166            # XXX we probably don't want to set _transferDecoder 
     167            self.channel._transferDecoder = WebSocketFrameDecoder( 
     168                self, handler) 
     169 
     170        # we need the nonce from the request body 
     171        self.channel._transferDecoder = _IdentityTransferDecoder(0, lambda _ : None, finishHandshake) 
     172 
     173 
    50174    def _checkClientHandshake(self): 
    51175        """ 
    52176        Verify client handshake, closing the connection in case of problem. 
     
    95219        connection will be closed. Otherwise, the response to the handshake is 
    96220        sent and a C{WebSocketHandler} is created to handle the request. 
    97221        """ 
    98         check =  self._checkClientHandshake() 
    99         if check is None: 
    100             return 
    101         originHeader, hostHeader, protocolHeader, handler = check 
    102         self.startedWriting = True 
    103         handshake = [ 
    104             "HTTP/1.1 101 Web Socket Protocol Handshake", 
    105             "Upgrade: WebSocket", 
    106             "Connection: Upgrade"] 
    107         handshake.append("WebSocket-Origin: %s" % (originHeader)) 
    108         if self.isSecure(): 
    109             scheme = "wss" 
     222        # check for post-75 handshake requests 
     223        isSecHandshake = self.requestHeaders.getRawHeaders("Sec-WebSocket-Key1", []) 
     224        if isSecHandshake: 
     225            self._clientHandshake76() 
    110226        else: 
    111             scheme = "ws" 
    112         handshake.append( 
    113             "WebSocket-Location: %s://%s%s" % ( 
    114             scheme, hostHeader, self.uri)) 
     227            check = self._checkClientHandshake() 
     228            if check is None: 
     229                return 
     230            originHeader, hostHeader, protocolHeader, handler = check 
     231            self.startedWriting = True 
     232            handshake = [ 
     233                "HTTP/1.1 101 Web Socket Protocol Handshake", 
     234                "Upgrade: WebSocket", 
     235                "Connection: Upgrade"] 
     236            handshake.append("WebSocket-Origin: %s" % (originHeader)) 
     237            if self.isSecure(): 
     238                scheme = "wss" 
     239            else: 
     240                scheme = "ws" 
     241            handshake.append( 
     242                "WebSocket-Location: %s://%s%s" % ( 
     243                scheme, hostHeader, self.uri)) 
    115244 
    116         if protocolHeader is not None: 
    117             handshake.append("WebSocket-Protocol: %s" % protocolHeader) 
     245            if protocolHeader is not None: 
     246                handshake.append("WebSocket-Protocol: %s" % protocolHeader) 
    118247 
    119         for header in handshake: 
    120             self.write("%s\r\n" % header) 
     248            for header in handshake: 
     249                self.write("%s\r\n" % header) 
    121250 
    122         self.write("\r\n") 
    123         self.channel.setRawMode() 
    124         # XXX we probably don't want to set _transferDecoder 
    125         self.channel._transferDecoder = WebSocketFrameDecoder( 
    126             self, handler) 
    127         return 
     251            self.write("\r\n") 
     252            self.channel.setRawMode() 
     253            # XXX we probably don't want to set _transferDecoder 
     254            self.channel._transferDecoder = WebSocketFrameDecoder( 
     255                self, handler) 
     256            return 
    128257 
    129258 
    130259 
     
    316445 
    317446 
    318447___all__ = ["WebSocketHandler", "WebSocketSite"] 
     448