Ticket #4173: rlotun-websocket-76.patch

File rlotun-websocket-76.patch, 10.1 KB (added by rlotun, 6 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