├── AUTHORS ├── txsockjs ├── tests │ ├── __init__.py │ ├── test_protocols.py │ ├── test_utils.py │ ├── common.py │ └── test_factory.py ├── protocols │ ├── __init__.py │ ├── eventsource.py │ ├── jsonp.py │ ├── htmlfile.py │ ├── xhr.py │ ├── static.py │ ├── websocket.py │ └── base.py ├── __init__.py ├── utils.py ├── multiplex.py ├── factory.py ├── websockets.py └── oldwebsockets.py ├── MANIFEST.in ├── .travis.yml ├── qunit ├── html │ ├── config.js │ ├── iframe.html │ ├── sockjs-in-head.html │ ├── index.html │ ├── unittests.html │ ├── tests-qunit.html │ ├── smoke-reconnect.html │ ├── smoke-latency.html │ ├── lib │ │ ├── endtoendtests.js │ │ ├── domtests.js │ │ ├── unittests.js │ │ └── tests.js │ ├── example-cursors.html │ └── static │ │ ├── qunit.css │ │ └── qunit.min.js └── server.py ├── .gitignore ├── .gitattributes ├── CHANGELOG.rst ├── LICENSE ├── twisted └── plugins │ └── sockjs_endpoints.py ├── setup.py └── README.rst /AUTHORS: -------------------------------------------------------------------------------- 1 | Christopher Gamble -------------------------------------------------------------------------------- /txsockjs/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /txsockjs/protocols/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /txsockjs/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.2.2" 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include README.rst -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: "2.7" 3 | install: pip install Twisted --use-mirrors 4 | script: trial tests 5 | notifications: 6 | email: 7 | - Fugiman47@gmail.com -------------------------------------------------------------------------------- /qunit/html/config.js: -------------------------------------------------------------------------------- 1 | var client_opts = { 2 | // Address of a sockjs test server. 3 | url: 'http://fugiman.com:8081', 4 | sockjs_opts: { 5 | devel: true, 6 | debug: true, 7 | // websocket:false 8 | info: {cookie_needed:false} 9 | } 10 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ############# 2 | ## Python 3 | ############# 4 | 5 | *.py[co] 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | 20 | # Installer logs 21 | pip-log.txt 22 | 23 | # Unit test / coverage reports 24 | .coverage 25 | .tox 26 | 27 | #Translations 28 | *.mo 29 | 30 | #Mr Developer 31 | .mr.developer.cfg 32 | 33 | # Mac crap 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /qunit/html/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 |

Don't panic!

13 | 20 | 21 | -------------------------------------------------------------------------------- /qunit/html/sockjs-in-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 17 | 18 | 19 | 20 |

Don't panic!

21 | 22 | -------------------------------------------------------------------------------- /qunit/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |

SockJS

12 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /txsockjs/tests/test_protocols.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from .common import EchoFactory, Request, BaseUnitTest 5 | 6 | HTTP_METHODS = ["OPTIONS","HEAD","GET","POST","PUT","DELETE"] 7 | 8 | class ProtocolUnitTest(BaseUnitTest): 9 | methods = ["OPTIONS"] 10 | 11 | def test_405(self): 12 | methods = list(set(HTTP_METHODS).difference(set(self.methods))) 13 | for m in methods: 14 | self.request.method = m 15 | self._load() 16 | self.assertEqual(self.request.responseCode, 405) 17 | self.assertFalse(self.request.responseHeaders.hasHeader("content-type")) 18 | self.assertTrue(self.request.responseHeaders.hasHeader("allow")) 19 | self.assertFalse(self.request.value()) 20 | -------------------------------------------------------------------------------- /txsockjs/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from twisted.trial import unittest 5 | from txsockjs import utils 6 | 7 | class MockTransport(object): 8 | value = "" 9 | def writeRaw(self, data): 10 | self.value += data 11 | 12 | class UtilsTestCase(unittest.TestCase): 13 | encoding = "cp1252" 14 | def test_normalize(self): 15 | for s in ["Hello!",u"こんにちは!",("Hello!",u"こんにちは!"),{"Hello!":u"こんにちは!"}]: 16 | n = utils.normalize(s, self.encoding) 17 | self.assertTrue(isinstance(n, str)) 18 | self.assertEqual(n, n.decode('utf-8', 'ignore').encode('utf-8', 'ignore')) 19 | 20 | def test_broadcast(self): 21 | targets = [MockTransport(), MockTransport(), MockTransport()] 22 | utils.broadcast("Hello!", targets, self.encoding) 23 | for t in targets: 24 | self.assertEqual(t.value, 'a["Hello!"]') 25 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | SockJS-Twisted Changelog 3 | ======================== 4 | 5 | 1.2 6 | === 7 | 8 | **1.2.2** 9 | * Fix CVE-2014-4671 10 | * Fix numerous bugs in which unicode wasn't converted to UTF-8 11 | * Fix heartbeats not being sent on websocket transport 12 | * Add MIT license headers to enable using txsockjs in Debian packages 13 | 14 | **1.2.1** 15 | * Fix broken setup.py 16 | 17 | **1.2.0** 18 | * Add endpoint support 19 | 20 | 1.1 21 | === 22 | 23 | **1.1.1** 24 | * Add python 2.6 compatability 25 | * Fix heartbeats not being sent until data was written from wrapped protocol 26 | 27 | **1.1.0** 28 | * Add proxy_header to expose proxied IPs 29 | * Remove pubsub from multiplexing as it was misleading 30 | 31 | 1.0 32 | === 33 | 34 | **1.0.0** 35 | * Refactor entire library to use twisted.web 36 | * Use t.w.websockets for websocket support instead of txWS 37 | * Add experimental multiplexing and pubsub functionality 38 | 39 | 0.1 40 | === 41 | 42 | **0.1.1** 43 | * Converts all text to UTF-8 (prevents crashing websockets in chrome) 44 | * Minor bug fixes 45 | 46 | **0.1.0** 47 | * Initial release 48 | -------------------------------------------------------------------------------- /qunit/html/unittests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

20 |

SockJS Unit Tests

21 |

22 |
23 |

24 |
    25 |
    test markup
    26 |

    27 |
    28 | 29 | 30 | 31 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Christopher Gamble 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Christopher Gamble nor the names of its 12 | contributors may be used to endorse or promote products derived 13 | from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 19 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 20 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 23 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 24 | OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /qunit/html/tests-qunit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |

    27 |

    SockJS Test Suite

    28 |

    29 |
    30 |

    31 |
      32 |
      test markup
      33 |

      34 |
      35 | 36 | 37 | 38 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /txsockjs/tests/common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from StringIO import StringIO 5 | from twisted.internet.protocol import Protocol, Factory 6 | from twisted.trial import unittest 7 | from twisted.web.test.test_web import DummyRequest 8 | from twisted.test.proto_helpers import StringTransport 9 | from twisted.internet.defer import succeed 10 | from twisted.internet.defer import inlineCallbacks 11 | from txsockjs.factory import SockJSFactory 12 | 13 | class EchoProtocol(Protocol): 14 | def dataReceived(self, data): 15 | self.transport.write(data) 16 | 17 | class EchoFactory(Factory): 18 | protocol = EchoProtocol 19 | 20 | class Request(DummyRequest): 21 | def __init__(self, method, *args, **kwargs): 22 | DummyRequest.__init__(self, *args, **kwargs) 23 | self.method = method 24 | self.content = StringIO() 25 | self.transport = StringTransport() 26 | 27 | def writeContent(self, data): 28 | self.content.seek(0,2) # Go to end of content 29 | self.content.write(data) # Write the data 30 | self.content.seek(0,0) # Go back to beginning of content 31 | 32 | def write(self, data): 33 | DummyRequest.write(self, data) 34 | self.transport.write("".join(self.written)) 35 | self.written = [] 36 | 37 | def value(self): 38 | return self.transport.value() 39 | 40 | class BaseUnitTest(unittest.TestCase): 41 | path = [''] 42 | 43 | def setUp(self): 44 | self.site = SockJSFactory(EchoFactory()) 45 | self.request = Request(self.path) 46 | 47 | @inlineCallbacks 48 | def _load(self): 49 | self.resource = self.site.getResourceFor(self.request) 50 | yield self._render(self.resource, self.request) 51 | 52 | def _render(resource, request): 53 | result = resource.render(request) 54 | if isinstance(result, str): 55 | request.write(result) 56 | request.finish() 57 | return succeed(None) 58 | elif result is server.NOT_DONE_YET: 59 | if request.finished: 60 | return succeed(None) 61 | else: 62 | return request.notifyFinish() 63 | else: 64 | raise ValueError("Unexpected return value: %r" % (result,)) 65 | -------------------------------------------------------------------------------- /txsockjs/protocols/eventsource.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012, Christopher Gamble 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the Christopher Gamble nor the names of its 12 | # contributors may be used to endorse or promote products derived 13 | # from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 19 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 20 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 23 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 24 | # OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | from txsockjs.protocols.base import StubResource 27 | 28 | class EventSource(StubResource): 29 | sent = 0 30 | done = False 31 | 32 | def render_GET(self, request): 33 | self.parent.setBaseHeaders(request) 34 | request.setHeader('content-type', 'text/event-stream; charset=UTF-8') 35 | request.write("\r\n") 36 | return self.connect(request) 37 | 38 | def write(self, data): 39 | if self.done: 40 | self.session.requeue([data]) 41 | return 42 | packet = "data: {0}\r\n\r\n".format(data) 43 | self.sent += len(packet) 44 | self.request.write(packet) 45 | if self.sent > self.parent._options['streaming_limit']: 46 | self.done = True 47 | self.disconnect() 48 | 49 | def writeSequence(self, data): 50 | for d in data: 51 | self.write(d) 52 | -------------------------------------------------------------------------------- /txsockjs/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012, Christopher Gamble 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the Christopher Gamble nor the names of its 12 | # contributors may be used to endorse or promote products derived 13 | # from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 19 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 20 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 23 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 24 | # OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | import json 27 | 28 | def normalize(s, encoding): 29 | if not isinstance(s, basestring): 30 | try: 31 | return str(s) 32 | except UnicodeEncodeError: 33 | return unicode(s).encode('utf-8','backslashreplace') 34 | elif isinstance(s, unicode): 35 | return s.encode('utf-8', 'backslashreplace') 36 | else: 37 | if s.decode('utf-8', 'ignore').encode('utf-8', 'ignore') == s: # Ensure s is a valid UTF-8 string 38 | return s 39 | else: # Otherwise assume it is Windows 1252 40 | return s.decode(encoding, 'replace').encode('utf-8', 'backslashreplace') 41 | 42 | def broadcast(message, targets, encoding="cp1252"): 43 | message = normalize(message, encoding) 44 | json_msg = 'a{0}'.format(json.dumps([message], separators=(',',':'))) 45 | for t in targets: 46 | if getattr(t, "writeRaw", None) is not None: 47 | t.writeRaw(json_msg) 48 | else: 49 | t.write(message) 50 | 51 | try: 52 | from twisted.internet.ssl import DefaultOpenSSLContextFactory 53 | # The only difference is using ctx.use_certificate_chain_file instead of ctx.use_certificate_file 54 | class ChainedOpenSSLContextFactory(DefaultOpenSSLContextFactory): 55 | def cacheContext(self): 56 | DefaultOpenSSLContextFactory.cacheContext(self) 57 | self._context.use_certificate_chain_file(self.certificateFileName) 58 | except ImportError: 59 | pass # no SSL support 60 | -------------------------------------------------------------------------------- /twisted/plugins/sockjs_endpoints.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012, Christopher Gamble 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the Christopher Gamble nor the names of its 12 | # contributors may be used to endorse or promote products derived 13 | # from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 19 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 20 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 23 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 24 | # OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | from zope.interface import implements 27 | from twisted.plugin import IPlugin 28 | from twisted.internet.interfaces import IStreamServerEndpointStringParser, IStreamServerEndpoint 29 | from twisted.internet.endpoints import serverFromString 30 | from txsockjs.factory import SockJSFactory 31 | 32 | class SockJSServerParser(object): 33 | implements(IPlugin, IStreamServerEndpointStringParser) 34 | 35 | prefix = "sockjs" 36 | 37 | def parseStreamServer(self, reactor, description, **options): 38 | if 'websocket' in options: 39 | options['websocket'] = options['websocket'].lower() == "true" 40 | 41 | if 'cookie_needed' in options: 42 | options['cookie_needed'] = options['cookie_needed'].lower() == "true" 43 | 44 | if 'heartbeat' in options: 45 | options['heartbeat'] = int(options['websocket']) 46 | 47 | if 'timeout' in options: 48 | options['timeout'] = int(options['timeout']) 49 | 50 | if 'streaming_limit' in options: 51 | options['streaming_limit'] = int(options['streaming_limit']) 52 | 53 | endpoint = serverFromString(reactor, description) 54 | return SockJSServerEndpoint(endpoint, options) 55 | 56 | class SockJSServerEndpoint(object): 57 | implements(IPlugin, IStreamServerEndpoint) 58 | 59 | def __init__(self, endpoint, options): 60 | self._endpoint = endpoint 61 | self._options = options 62 | 63 | def listen(self, protocolFactory): 64 | return self._endpoint.listen(SockJSFactory(protocolFactory, self._options)) 65 | 66 | SockJSServerParserInstance = SockJSServerParser() 67 | -------------------------------------------------------------------------------- /txsockjs/protocols/jsonp.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012, Christopher Gamble 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the Christopher Gamble nor the names of its 12 | # contributors may be used to endorse or promote products derived 13 | # from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 19 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 20 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 23 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 24 | # OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | from twisted.web import http 27 | from txsockjs.protocols.base import StubResource 28 | 29 | class JSONP(StubResource): 30 | written = False 31 | 32 | def render_GET(self, request): 33 | self.parent.setBaseHeaders(request) 34 | self.callback = request.args.get('c',[None])[0] 35 | if self.callback is None: 36 | request.setResponseCode(http.INTERNAL_SERVER_ERROR) 37 | return '"callback" parameter required' 38 | request.setHeader('content-type', 'application/javascript; charset=UTF-8') 39 | return self.connect(request) 40 | 41 | def write(self, data): 42 | if self.written: 43 | self.session.requeue([data]) 44 | return 45 | self.written = True 46 | self.request.write("/**/{0}(\"{1}\");\r\n".format(self.callback, data.replace('\\','\\\\').replace('"','\\"'))) 47 | self.disconnect() 48 | 49 | def writeSequence(self, data): 50 | self.write(data.pop(0)) 51 | self.session.requeue(data) 52 | 53 | class JSONPSend(StubResource): 54 | def render_POST(self, request): 55 | self.parent.setBaseHeaders(request) 56 | request.setHeader('content-type', 'text/plain; charset=UTF-8') 57 | urlencoded = request.getHeader("Content-Type") == 'application/x-www-form-urlencoded' 58 | data = request.args.get('d', [''])[0] if urlencoded else request.content.read() 59 | ret = self.session.dataReceived(data) 60 | if not ret: 61 | return "ok" 62 | request.setResponseCode(http.INTERNAL_SERVER_ERROR) 63 | return "{0}\r\n".format(ret) 64 | -------------------------------------------------------------------------------- /txsockjs/protocols/htmlfile.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012, Christopher Gamble 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the Christopher Gamble nor the names of its 12 | # contributors may be used to endorse or promote products derived 13 | # from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 19 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 20 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 23 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 24 | # OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | from twisted.web import http 27 | from txsockjs.protocols.base import StubResource 28 | 29 | class HTMLFile(StubResource): 30 | sent = 0 31 | done = False 32 | 33 | def render_GET(self, request): 34 | self.parent.setBaseHeaders(request) 35 | callback = request.args.get('c',[None])[0] 36 | if callback is None: 37 | request.setResponseCode(http.INTERNAL_SERVER_ERROR) 38 | return '"callback" parameter required' 39 | request.setHeader('content-type', 'text/html; charset=UTF-8') 40 | request.write(r''' 41 | 42 | 43 | 44 | 45 |

      Don't panic!

      46 | {1} 53 | '''.format(callback, ' '*1024)) 54 | return self.connect(request) 55 | 56 | def write(self, data): 57 | if self.done: 58 | self.session.requeue([data]) 59 | return 60 | packet = "\r\n".format(data.replace('\\','\\\\').replace('"','\\"')) 61 | self.sent += len(packet) 62 | self.request.write(packet) 63 | if self.sent > self.parent._options['streaming_limit']: 64 | self.done = True 65 | self.disconnect() 66 | 67 | def writeSequence(self, data): 68 | for d in data: 69 | self.write(d) 70 | -------------------------------------------------------------------------------- /qunit/html/smoke-reconnect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
      16 | 28 | 29 | 30 |
      31 | 32 | Connected:
      33 | 34 | 35 | 36 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /qunit/html/smoke-latency.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
      16 | 28 | 29 | 30 |
      31 | 32 | Latency:
      33 | 34 | 35 | 36 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # When pip installs anything from packages, py_modules, or ext_modules that 2 | # includes a twistd plugin (which are installed to twisted/plugins/), 3 | # setuptools/distribute writes a Package.egg-info/top_level.txt that includes 4 | # "twisted". If you later uninstall Package with `pip uninstall Package`, 5 | # pip <1.2 removes all of twisted/ instead of just Package's twistd plugins. 6 | # See https://github.com/pypa/pip/issues/355 (now fixed) 7 | # 8 | # To work around this problem, we monkeypatch 9 | # setuptools.command.egg_info.write_toplevel_names to not write the line 10 | # "twisted". This fixes the behavior of `pip uninstall Package`. Note that 11 | # even with this workaround, `pip uninstall Package` still correctly uninstalls 12 | # Package's twistd plugins from twisted/plugins/, since pip also uses 13 | # Package.egg-info/installed-files.txt to determine what to uninstall, 14 | # and the paths to the plugin files are indeed listed in installed-files.txt. 15 | from distutils import log 16 | from setuptools import setup 17 | from setuptools.command.install import install 18 | 19 | 20 | class InstallTwistedPlugin(install, object): 21 | def run(self): 22 | super(InstallTwistedPlugin, self).run() 23 | 24 | # Make Twisted regenerate the dropin.cache, if possible. This is necessary 25 | # because in a site-wide install, dropin.cache cannot be rewritten by 26 | # normal users. 27 | log.info("Attempting to update Twisted plugin cache.") 28 | try: 29 | from twisted.plugin import IPlugin, getPlugins 30 | list(getPlugins(IPlugin)) 31 | log.info("Twisted plugin cache updated successfully.") 32 | except Exception, e: 33 | log.warn("*** Failed to update Twisted plugin cache. ***") 34 | log.warn(str(e)) 35 | 36 | 37 | try: 38 | from setuptools.command import egg_info 39 | egg_info.write_toplevel_names 40 | except (ImportError, AttributeError): 41 | pass 42 | else: 43 | def _top_level_package(name): 44 | return name.split('.', 1)[0] 45 | 46 | def _hacked_write_toplevel_names(cmd, basename, filename): 47 | pkgs = dict.fromkeys( 48 | [_top_level_package(k) 49 | for k in cmd.distribution.iter_distribution_names() 50 | if _top_level_package(k) != "twisted" 51 | ] 52 | ) 53 | cmd.write_file("top-level names", filename, '\n'.join(pkgs) + '\n') 54 | 55 | egg_info.write_toplevel_names = _hacked_write_toplevel_names 56 | 57 | # Now actually define the setup 58 | import txsockjs 59 | import os 60 | 61 | setup( 62 | author="Christopher Gamble", 63 | author_email="chris@chrisgamble.net", 64 | name="txsockjs", 65 | version=txsockjs.__version__, 66 | description="Twisted SockJS wrapper", 67 | long_description=open(os.path.join(os.path.dirname(__file__), 'README.rst')).read(), 68 | url="http://github.com/Fugiman/sockjs-twisted", 69 | license='BSD License', 70 | platforms=['OS Independent'], 71 | packages=["txsockjs","txsockjs.protocols","twisted.plugins"], 72 | install_requires=[ 73 | "Twisted", 74 | ], 75 | classifiers=[ 76 | "Development Status :: 5 - Production/Stable", 77 | "Framework :: Twisted", 78 | "Intended Audience :: Developers", 79 | "License :: OSI Approved :: BSD License", 80 | "Operating System :: OS Independent", 81 | "Programming Language :: Python", 82 | "Topic :: Internet", 83 | ], 84 | cmdclass = { 85 | 'install': InstallTwistedPlugin, 86 | }, 87 | ) 88 | -------------------------------------------------------------------------------- /txsockjs/protocols/xhr.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012, Christopher Gamble 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the Christopher Gamble nor the names of its 12 | # contributors may be used to endorse or promote products derived 13 | # from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 19 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 20 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 23 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 24 | # OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | from twisted.web import resource, http 27 | from txsockjs.protocols.base import StubResource 28 | 29 | class XHR(StubResource): 30 | written = False 31 | 32 | def render_POST(self, request): 33 | self.parent.setBaseHeaders(request) 34 | request.setHeader('content-type', 'application/javascript; charset=UTF-8') 35 | return self.connect(request) 36 | 37 | def write(self, data): 38 | if self.written: 39 | self.session.requeue([data]) 40 | return 41 | self.written = True 42 | self.request.write("{0}\n".format(data)) 43 | self.disconnect() 44 | 45 | def writeSequence(self, data): 46 | if not self.written: 47 | self.write(data.pop(0)) 48 | self.session.requeue(data) 49 | 50 | class XHRSend(StubResource): 51 | def render_POST(self, request): 52 | self.parent.setBaseHeaders(request) 53 | request.setResponseCode(http.NO_CONTENT) 54 | request.setHeader('content-type', 'text/plain; charset=UTF-8') 55 | ret = self.session.dataReceived(request.content.read()) 56 | if not ret: 57 | return "" 58 | request.setResponseCode(http.INTERNAL_SERVER_ERROR) 59 | return "{0}\r\n".format(ret) 60 | 61 | class XHRStream(StubResource): 62 | sent = 0 63 | done = False 64 | 65 | def render_POST(self, request): 66 | self.parent.setBaseHeaders(request) 67 | request.setHeader('content-type', 'application/javascript; charset=UTF-8') 68 | request.write("{0}\n".format('h'*2048)) 69 | return self.connect(request) 70 | 71 | def write(self, data): 72 | if self.done: 73 | self.session.requeue([data]) 74 | return 75 | packet = "{0}\n".format(data) 76 | self.sent += len(packet) 77 | self.request.write(packet) 78 | if self.sent > self.parent._options['streaming_limit']: 79 | self.done = True 80 | self.disconnect() 81 | 82 | def writeSequence(self, data): 83 | for d in data: 84 | self.write(d) 85 | -------------------------------------------------------------------------------- /txsockjs/protocols/static.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012, Christopher Gamble 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the Christopher Gamble nor the names of its 12 | # contributors may be used to endorse or promote products derived 13 | # from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 19 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 20 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 23 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 24 | # OF THE POSSIBILITY OF SUCH DAMAGE. 25 | import json, random 26 | from twisted.web import resource, http 27 | 28 | class Info(resource.Resource): 29 | def render_GET(self, request): 30 | self.parent.setBaseHeaders(request,False) 31 | request.setHeader('content-type', 'application/json; charset=UTF-8') 32 | data = { 33 | 'websocket': self.parent._options['websocket'], 34 | 'cookie_needed': self.parent._options['cookie_needed'], 35 | 'origins': ['*:*'], 36 | 'entropy': random.randint(0,2**32-1) 37 | } 38 | return json.dumps(data) 39 | 40 | def render_OPTIONS(self, request): 41 | request.setResponseCode(http.NO_CONTENT) 42 | self.parent.setBaseHeaders(request,False) 43 | request.setHeader('Cache-Control', 'public, max-age=31536000') 44 | request.setHeader('access-control-max-age', '31536000') 45 | request.setHeader('Expires', 'Fri, 01 Jan 2500 00:00:00 GMT') #Get a new library by then 46 | request.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET') # Hardcoding this may be bad? 47 | return "" 48 | 49 | class IFrame(resource.Resource): 50 | etag = '00000000-0000-0000-0000-000000000000' 51 | 52 | def render_GET(self, request): 53 | self.parent.setBaseHeaders(request,False) 54 | if request.setETag(self.etag): 55 | request.setResponseCode(http.NOT_MODIFIED) 56 | return "" 57 | request.setHeader('content-type', 'text/html; charset=UTF-8') 58 | request.setHeader('Cache-Control', 'public, max-age=31536000') 59 | request.setHeader('access-control-max-age', '31536000') 60 | request.setHeader('Expires', 'Fri, 01 Jan 2500 00:00:00 GMT') #Get a new library by then 61 | return ''' 62 | 63 | 64 | 65 | 66 | 67 | 71 | 72 | 73 | 74 |

      Don't panic!

      75 |

      This is a SockJS hidden iframe. It's used for cross domain magic.

      76 | 77 | '''.format(self.parent._options["sockjs_url"]) 78 | -------------------------------------------------------------------------------- /qunit/html/lib/endtoendtests.js: -------------------------------------------------------------------------------- 1 | var factory_body_check; 2 | 3 | module('End to End'); 4 | 5 | factory_body_check = function(protocol) { 6 | var n; 7 | if (!SockJS[protocol] || !SockJS[protocol].enabled(client_opts.sockjs_opts)) { 8 | n = " " + protocol + " [unsupported by client]"; 9 | return test(n, function() { 10 | return log('Unsupported protocol (by client): "' + protocol + '"'); 11 | }); 12 | } else { 13 | return asyncTest(protocol, function() { 14 | var code, hook, url; 15 | expect(5); 16 | url = client_opts.url + '/echo'; 17 | code = "hook.test_body(!!document.body, typeof document.body);\n\nvar sock = new SockJS('" + url + "', null,\n{protocols_whitelist:['" + protocol + "']});\nsock.onopen = function() {\n var m = hook.onopen();\n sock.send(m);\n};\nsock.onmessage = function(e) {\n hook.onmessage(e.data);\n sock.close();\n};"; 18 | hook = newIframe('sockjs-in-head.html'); 19 | hook.open = function() { 20 | hook.iobj.loaded(); 21 | ok(true, 'open'); 22 | return hook.callback(code); 23 | }; 24 | hook.test_body = function(is_body, type) { 25 | return equal(is_body, false, 'body not yet loaded ' + type); 26 | }; 27 | hook.onopen = function() { 28 | ok(true, 'onopen'); 29 | return 'a'; 30 | }; 31 | return hook.onmessage = function(m) { 32 | equal(m, 'a'); 33 | ok(true, 'onmessage'); 34 | hook.iobj.cleanup(); 35 | hook.del(); 36 | return start(); 37 | }; 38 | }); 39 | } 40 | }; 41 | 42 | module('connection errors'); 43 | 44 | asyncTest("invalid url 404", function() { 45 | var r; 46 | expect(4); 47 | r = newSockJS('/invalid_url', 'jsonp-polling'); 48 | ok(r); 49 | r.onopen = function(e) { 50 | return ok(false); 51 | }; 52 | r.onmessage = function(e) { 53 | return ok(false); 54 | }; 55 | return r.onclose = function(e) { 56 | if (u.isXHRCorsCapable() < 4) { 57 | equals(e.code, 1002); 58 | equals(e.reason, 'Can\'t connect to server'); 59 | } else { 60 | equals(e.code, 2000); 61 | equals(e.reason, 'All transports failed'); 62 | } 63 | equals(e.wasClean, false); 64 | return start(); 65 | }; 66 | }); 67 | 68 | asyncTest("invalid url port", function() { 69 | var dl, r; 70 | expect(4); 71 | dl = document.location; 72 | r = newSockJS(dl.protocol + '//' + dl.hostname + ':1079', 'jsonp-polling'); 73 | ok(r); 74 | r.onopen = function(e) { 75 | return ok(false); 76 | }; 77 | return r.onclose = function(e) { 78 | if (u.isXHRCorsCapable() < 4) { 79 | equals(e.code, 1002); 80 | equals(e.reason, 'Can\'t connect to server'); 81 | } else { 82 | equals(e.code, 2000); 83 | equals(e.reason, 'All transports failed'); 84 | } 85 | equals(e.wasClean, false); 86 | return start(); 87 | }; 88 | }); 89 | 90 | asyncTest("disabled websocket test", function() { 91 | var r; 92 | expect(3); 93 | r = newSockJS('/disabled_websocket_echo', 'websocket'); 94 | r.onopen = function(e) { 95 | return ok(false); 96 | }; 97 | r.onmessage = function(e) { 98 | return ok(false); 99 | }; 100 | return r.onclose = function(e) { 101 | equals(e.code, 2000); 102 | equals(e.reason, "All transports failed"); 103 | equals(e.wasClean, false); 104 | return start(); 105 | }; 106 | }); 107 | 108 | asyncTest("close on close", function() { 109 | var r; 110 | expect(4); 111 | r = newSockJS('/close', 'jsonp-polling'); 112 | r.onopen = function(e) { 113 | return ok(true); 114 | }; 115 | r.onmessage = function(e) { 116 | return ok(false); 117 | }; 118 | return r.onclose = function(e) { 119 | equals(e.code, 3000); 120 | equals(e.reason, "Go away!"); 121 | equals(e.wasClean, true); 122 | r.onclose = function() { 123 | return ok(false); 124 | }; 125 | r.close(); 126 | return u.delay(10, function() { 127 | return start(); 128 | }); 129 | }; 130 | }); 131 | 132 | asyncTest("EventEmitter exception handling", function() { 133 | var prev_onerror, r; 134 | expect(1); 135 | r = newSockJS('/echo', 'xhr-streaming'); 136 | prev_onerror = window.onerror; 137 | window.onerror = function(e) { 138 | ok(/onopen error/.test('' + e)); 139 | window.onerror = prev_onerror; 140 | return r.close(); 141 | }; 142 | r.onopen = function(e) { 143 | throw "onopen error"; 144 | }; 145 | return r.onclose = function() { 146 | return start(); 147 | }; 148 | }); 149 | -------------------------------------------------------------------------------- /txsockjs/tests/test_factory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from twisted.web.resource import NoResource 5 | from txsockjs.factory import SockJSFactory, SockJSResource 6 | from txsockjs.protocols.eventsource import EventSource 7 | from txsockjs.protocols.htmlfile import HTMLFile 8 | from txsockjs.protocols.jsonp import JSONP, JSONPSend 9 | from txsockjs.protocols.static import Info, IFrame 10 | from txsockjs.protocols.websocket import RawWebSocket, WebSocket 11 | from txsockjs.protocols.xhr import XHR, XHRSend, XHRStream 12 | from .common import EchoFactory, Request, BaseUnitTest 13 | 14 | class FactoryUnitTest(BaseUnitTest): 15 | valid_sessions = (['a','a'],['_','_'],['1','1'],['abcdefgh_i-j%20','abcdefgh_i-j%20']) 16 | invalid_sessions = (['',''],['a.','a'],['a','a.'],['.','.'],[''],['','','']) 17 | 18 | def setUp(self): 19 | self.site = SockJSFactory(EchoFactory()) 20 | 21 | def _test(self, path, resource): 22 | req = Request("OPTIONS", path) 23 | # Also tests that OPTIONS requests don't produce upstream connections 24 | res = self.site.getResourceFor(req) 25 | self.assertTrue(isinstance(res, resource)) 26 | 27 | def _test_wrapper(self, path, resource): 28 | for s in self.valid_sessions: 29 | self._test(s + [path], resource) 30 | self._test(s + [path,''], NoResource) 31 | for s in self.invalid_sessions: 32 | self._test(s + [path], NoResource) 33 | self._test(s + [path,''], NoResource) 34 | 35 | def test_greeting(self): 36 | self._test([], SockJSResource) 37 | self._test([''], SockJSResource) 38 | 39 | def test_info(self): 40 | self._test(['info'], Info) 41 | self._test(['info',''], NoResource) 42 | 43 | def test_iframe(self): 44 | self._test(['iframe.html'], IFrame) 45 | self._test(['iframe-a.html'], IFrame) 46 | self._test(['iframe-.html'], IFrame) 47 | self._test(['iframe-0.1.2.html'], IFrame) 48 | self._test(['iframe-0.1.2abc-dirty.2144.html'], IFrame) 49 | self._test(['iframe.htm'], NoResource) 50 | self._test(['iframe'], NoResource) 51 | self._test(['IFRAME.HTML'], NoResource) 52 | self._test(['IFRAME'], NoResource) 53 | self._test(['iframe.HTML'], NoResource) 54 | self._test(['iframe.xml'], NoResource) 55 | self._test(['iframe-','.html'], NoResource) 56 | 57 | def test_rawwebsocket(self): 58 | self._test(['websocket'], RawWebSocket) 59 | self._test(['websocket',''], RawWebSocket) 60 | 61 | def test_websocket(self): 62 | self._test_wrapper('websocket', WebSocket) 63 | 64 | def test_eventsource(self): 65 | self._test_wrapper('eventsource', EventSource) 66 | 67 | def test_htmlfile(self): 68 | self._test_wrapper('htmlfile', HTMLFile) 69 | 70 | def test_xhr_stream(self): 71 | self._test_wrapper('xhr_streaming', XHRStream) 72 | 73 | def test_xhr(self): 74 | self._test_wrapper('xhr', XHR) 75 | 76 | def test_jsonp(self): 77 | self._test_wrapper('jsonp', JSONP) 78 | 79 | def test_xhr_send(self): 80 | self._test_wrapper('xhr_send', XHRSend) 81 | 82 | def test_jsonp_send(self): 83 | self._test_wrapper('jsonp_send', JSONPSend) 84 | 85 | def test_invalid_endpoint(self): 86 | self._test(['a','a','a'], NoResource) 87 | 88 | def test_nonexistant_session_write(self): 89 | req = Request("POST", ['a','a','xhr_send']) 90 | res = self.site.getResourceFor(req) 91 | self.assertTrue(isinstance(res, NoResource)) 92 | 93 | def test_ignore_server_id(self): 94 | # Open session 95 | req = Request("POST", ['000','a','xhr']) 96 | res = self.site.getResourceFor(req) 97 | yield self._render(res, req) 98 | self.assertEqual(req.value(), 'o\n') 99 | # Write data to session 100 | req = Request("POST", ['000','a','xhr_send']) 101 | req.writeContent('["a"]') 102 | res = self.site.getResourceFor(req) 103 | yield self._render(res, req) 104 | # Ensure it appears despite different Server ID 105 | req = Request("POST", ['999','a','xhr']) 106 | res = self.site.getResourceFor(req) 107 | yield self._render(res, req) 108 | self.assertEqual(req.value(), 'a["a"]\n') 109 | # Clean up 110 | for p in self.site.resource._sessions.values(): 111 | p.disconnect() 112 | -------------------------------------------------------------------------------- /qunit/html/example-cursors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 23 | 24 | 25 | 26 | 27 |
      28 | 41 | 42 | 43 |
      44 | 45 | Latency:
      46 | 47 | 48 | 49 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /txsockjs/multiplex.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012, Christopher Gamble 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the Christopher Gamble nor the names of its 12 | # contributors may be used to endorse or promote products derived 13 | # from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 19 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 20 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 23 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 24 | # OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | from twisted.internet.protocol import Protocol, Factory 27 | from twisted.protocols.policies import ProtocolWrapper 28 | from txsockjs.factory import SockJSResource 29 | from txsockjs import utils 30 | 31 | class MultiplexProxy(ProtocolWrapper): 32 | def __init__(self, factory, wrappedProtocol, transport, topic): 33 | ProtocolWrapper.__init__(self, factory, wrappedProtocol) 34 | self.topic = topic 35 | self.makeConnection(transport) 36 | 37 | def write(self, data): 38 | self.transport.transport.write(",".join(["msg", self.topic, data])) 39 | 40 | def writeSequence(self, data): 41 | for d in data: 42 | self.write(d) 43 | 44 | def broadcast(self, data): 45 | self.factory.broadcast(self.topic, data) 46 | 47 | def loseConnection(self): 48 | self.transport.transport.write(",".join(["uns", self.topic])) 49 | 50 | class MultiplexProtocol(Protocol): 51 | def connectionMade(self): 52 | self.factory._connections[self] = {} 53 | 54 | def dataReceived(self, message): 55 | type, chaff, topic = message.partition(",") 56 | if "," in topic: 57 | topic, chaff, payload = topic.partition(",") 58 | if type == "sub": 59 | self.factory.subscribe(self, topic) 60 | elif type == "msg": 61 | self.factory.handleMessage(self, topic, payload) 62 | elif type == "uns": 63 | self.factory.unsubscribe(self, topic) 64 | 65 | def connectionLost(self, reason=None): 66 | for conn in self.factory._connections[self].values(): 67 | conn.connectionLost(reason) 68 | del self.factory._connections[self] 69 | 70 | class MultiplexFactory(Factory): 71 | protocol = MultiplexProtocol 72 | 73 | def __init__(self, resource): 74 | self._resource = resource 75 | self._topics = {} 76 | self._connections = {} 77 | 78 | def addFactory(self, name, factory): 79 | self._topics[name] = factory 80 | 81 | def broadcast(self, name, message): 82 | targets = [] 83 | message = ",".join(["msg", name, message]) 84 | for p, topics in self._connections.items(): 85 | if name in topics: 86 | targets.append(p) 87 | utils.broadcast(message, targets) 88 | 89 | def removeFactory(self, name, factory): 90 | del self._topics[name] 91 | 92 | def subscribe(self, p, name): 93 | if name not in self._topics: 94 | return 95 | self._connections[p][name] = MultiplexProxy(self, self._topics[name].buildProtocol(p.transport.getPeer()), p, name) 96 | 97 | def handleMessage(self, p, name, message): 98 | if p not in self._connections: 99 | return 100 | if name not in self._connections[p]: 101 | return 102 | self._connections[p][name].dataReceived(message) 103 | 104 | def unsubscribe(self, p, name): 105 | if p not in self._connections: 106 | return 107 | if name not in self._connections[p]: 108 | return 109 | self._connections[p][name].connectionLost(None) 110 | del self._connections[p][name] 111 | 112 | def registerProtocol(self, p): 113 | pass 114 | 115 | def unregisterProtocol(self, p): 116 | pass 117 | 118 | class SockJSMultiplexResource(SockJSResource): 119 | def __init__(self, options=None): 120 | SockJSResource.__init__(self, MultiplexFactory(self), options) 121 | 122 | def addFactory(self, name, factory): 123 | return self._factory.addFactory(name, factory) 124 | 125 | def broadcast(self, name, message): 126 | return self._factory.broadcast(name, message) 127 | 128 | def removeFactory(self, name): 129 | return self._factory.removeFactory(name) 130 | -------------------------------------------------------------------------------- /qunit/html/static/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 15px 15px 0 0; 42 | -moz-border-radius: 15px 15px 0 0; 43 | -webkit-border-top-right-radius: 15px; 44 | -webkit-border-top-left-radius: 15px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-banner { 58 | height: 5px; 59 | } 60 | 61 | #qunit-testrunner-toolbar { 62 | padding: 0.5em 0 0.5em 2em; 63 | color: #5E740B; 64 | background-color: #eee; 65 | } 66 | 67 | #qunit-userAgent { 68 | padding: 0.5em 0 0.5em 2.5em; 69 | background-color: #2b81af; 70 | color: #fff; 71 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 72 | } 73 | 74 | 75 | /** Tests: Pass/Fail */ 76 | 77 | #qunit-tests { 78 | list-style-position: inside; 79 | } 80 | 81 | #qunit-tests li { 82 | padding: 0.4em 0.5em 0.4em 2.5em; 83 | border-bottom: 1px solid #fff; 84 | list-style-position: inside; 85 | } 86 | 87 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 88 | display: none; 89 | } 90 | 91 | #qunit-tests li strong { 92 | cursor: pointer; 93 | } 94 | 95 | #qunit-tests li a { 96 | padding: 0.5em; 97 | color: #c2ccd1; 98 | text-decoration: none; 99 | } 100 | #qunit-tests li a:hover, 101 | #qunit-tests li a:focus { 102 | color: #000; 103 | } 104 | 105 | #qunit-tests ol { 106 | margin-top: 0.5em; 107 | padding: 0.5em; 108 | 109 | background-color: #fff; 110 | 111 | border-radius: 15px; 112 | -moz-border-radius: 15px; 113 | -webkit-border-radius: 15px; 114 | 115 | box-shadow: inset 0px 2px 13px #999; 116 | -moz-box-shadow: inset 0px 2px 13px #999; 117 | -webkit-box-shadow: inset 0px 2px 13px #999; 118 | } 119 | 120 | #qunit-tests table { 121 | border-collapse: collapse; 122 | margin-top: .2em; 123 | } 124 | 125 | #qunit-tests th { 126 | text-align: right; 127 | vertical-align: top; 128 | padding: 0 .5em 0 0; 129 | } 130 | 131 | #qunit-tests td { 132 | vertical-align: top; 133 | } 134 | 135 | #qunit-tests pre { 136 | margin: 0; 137 | white-space: pre-wrap; 138 | word-wrap: break-word; 139 | } 140 | 141 | #qunit-tests del { 142 | background-color: #e0f2be; 143 | color: #374e0c; 144 | text-decoration: none; 145 | } 146 | 147 | #qunit-tests ins { 148 | background-color: #ffcaca; 149 | color: #500; 150 | text-decoration: none; 151 | } 152 | 153 | /*** Test Counts */ 154 | 155 | #qunit-tests b.counts { color: black; } 156 | #qunit-tests b.passed { color: #5E740B; } 157 | #qunit-tests b.failed { color: #710909; } 158 | 159 | #qunit-tests li li { 160 | margin: 0.5em; 161 | padding: 0.4em 0.5em 0.4em 0.5em; 162 | background-color: #fff; 163 | border-bottom: none; 164 | list-style-position: inside; 165 | } 166 | 167 | /*** Passing Styles */ 168 | 169 | #qunit-tests li li.pass { 170 | color: #5E740B; 171 | background-color: #fff; 172 | border-left: 26px solid #C6E746; 173 | } 174 | 175 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 176 | #qunit-tests .pass .test-name { color: #366097; } 177 | 178 | #qunit-tests .pass .test-actual, 179 | #qunit-tests .pass .test-expected { color: #999999; } 180 | 181 | #qunit-banner.qunit-pass { background-color: #C6E746; } 182 | 183 | /*** Failing Styles */ 184 | 185 | #qunit-tests li li.fail { 186 | color: #710909; 187 | background-color: #fff; 188 | border-left: 26px solid #EE5757; 189 | } 190 | 191 | #qunit-tests > li:last-child { 192 | border-radius: 0 0 15px 15px; 193 | -moz-border-radius: 0 0 15px 15px; 194 | -webkit-border-bottom-right-radius: 15px; 195 | -webkit-border-bottom-left-radius: 15px; 196 | } 197 | 198 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 199 | #qunit-tests .fail .test-name, 200 | #qunit-tests .fail .module-name { color: #000000; } 201 | 202 | #qunit-tests .fail .test-actual { color: #EE5757; } 203 | #qunit-tests .fail .test-expected { color: green; } 204 | 205 | #qunit-banner.qunit-fail { background-color: #EE5757; } 206 | 207 | 208 | /** Result */ 209 | 210 | #qunit-testresult { 211 | padding: 0.5em 0.5em 0.5em 2.5em; 212 | 213 | color: #2b81af; 214 | background-color: #D2E0E6; 215 | 216 | border-bottom: 1px solid white; 217 | } 218 | 219 | /** Fixture */ 220 | 221 | #qunit-fixture { 222 | position: absolute; 223 | top: -10000px; 224 | left: -10000px; 225 | } 226 | -------------------------------------------------------------------------------- /qunit/html/lib/domtests.js: -------------------------------------------------------------------------------- 1 | var ajax_simple_factory, ajax_streaming_factory, ajax_wrong_port_factory, newIframe, onunload_test_factory, test_wrong_url, u; 2 | 3 | module('Dom'); 4 | 5 | u = SockJS.getUtils(); 6 | 7 | newIframe = function(path) { 8 | var err, hook; 9 | if (path == null) path = '/iframe.html'; 10 | hook = u.createHook(); 11 | err = function() { 12 | return log('iframe error. bad.'); 13 | }; 14 | hook.iobj = u.createIframe(path + '?a=' + Math.random() + '#' + hook.id, err); 15 | return hook; 16 | }; 17 | 18 | onunload_test_factory = function(code) { 19 | return function() { 20 | var hook; 21 | expect(3); 22 | hook = newIframe(); 23 | hook.open = function() { 24 | ok(true, 'open hook called by an iframe'); 25 | return hook.callback(code); 26 | }; 27 | hook.load = function() { 28 | var f; 29 | ok(true, 'onload hook called by an iframe'); 30 | f = function() { 31 | return hook.iobj.cleanup(); 32 | }; 33 | return setTimeout(f, 1); 34 | }; 35 | return hook.unload = function() { 36 | ok(true, 'onunload hook called by an iframe'); 37 | hook.del(); 38 | return start(); 39 | }; 40 | }; 41 | }; 42 | 43 | if (navigator.userAgent.indexOf('Konqueror') !== -1 || navigator.userAgent.indexOf('Opera') !== -1) { 44 | test("onunload [unsupported by client]", function() { 45 | return ok(true); 46 | }); 47 | } else { 48 | asyncTest('onunload', onunload_test_factory("var u = SockJS.getUtils();\nu.attachEvent('load', function(){\n hook.load();\n});\nvar w = 0;\nvar run = function(){\n if(w === 0) {\n w = 1;\n hook.unload();\n }\n};\nu.attachEvent('beforeunload', run);\nu.attachEvent('unload', run);")); 49 | } 50 | 51 | if (!SockJS.getIframeTransport().enabled()) { 52 | test("onmessage [unsupported by client]", function() { 53 | return ok(true); 54 | }); 55 | } else { 56 | asyncTest('onmessage', function() { 57 | var hook; 58 | expect(3); 59 | hook = newIframe(); 60 | hook.open = function() { 61 | ok(true, 'open hook called by an iframe'); 62 | return hook.callback("var u = SockJS.getUtils();\nu.attachMessage(function(e) {\n var b = e.data;\n parent.postMessage(window_id + ' ' + 'e', '*');\n});\nparent.postMessage(window_id + ' ' + 's', '*');"); 63 | }; 64 | return u.attachMessage(function(e) { 65 | var data, origin, window_id, _ref; 66 | _ref = e.data.split(' '), window_id = _ref[0], data = _ref[1]; 67 | if (window_id === hook.id) { 68 | switch (data) { 69 | case 's': 70 | hook.iobj.loaded(); 71 | ok(true, 'start frame send'); 72 | origin = u.getOrigin(u.amendUrl('/')); 73 | return hook.iobj.post(hook.id + ' ' + 's', origin); 74 | case 'e': 75 | ok(true, 'done hook called by an iframe'); 76 | hook.iobj.cleanup(); 77 | hook.del(); 78 | return start(); 79 | } 80 | } 81 | }); 82 | }); 83 | } 84 | 85 | ajax_simple_factory = function(name) { 86 | return asyncTest(name + ' simple', function() { 87 | var x; 88 | expect(2); 89 | x = new u[name]('GET', '/simple.txt', null); 90 | return x.onfinish = function(status, text) { 91 | equal(text.length, 2051); 92 | equal(text.slice(-2), 'b\n'); 93 | return start(); 94 | }; 95 | }); 96 | }; 97 | 98 | ajax_streaming_factory = function(name) { 99 | return asyncTest(name + ' streaming', function() { 100 | var x; 101 | expect(4); 102 | x = new u[name]('GET', '/streaming.txt', null); 103 | x.onchunk = function(status, text) { 104 | equal(status, 200); 105 | ok(text.length <= 2049, 'Most likely you\'re behind a transparent Proxy that can\'t do streaming. QUnit tests won\'t work properly. Sorry!'); 106 | return delete x.onchunk; 107 | }; 108 | return x.onfinish = function(status, text) { 109 | equal(status, 200); 110 | equal(text.slice(-4), 'a\nb\n'); 111 | return start(); 112 | }; 113 | }); 114 | }; 115 | 116 | test_wrong_url = function(name, url, statuses) { 117 | var x; 118 | if (window.console && console.log) { 119 | console.log(' [*] Connecting to wrong url ' + url); 120 | } 121 | expect(2); 122 | x = new u[name]('GET', url, null); 123 | x.onchunk = function() { 124 | return ok(false, "chunk shall not be received"); 125 | }; 126 | return x.onfinish = function(status, text) { 127 | ok(u.arrIndexOf(statuses, status) !== -1); 128 | equal(text, ''); 129 | return start(); 130 | }; 131 | }; 132 | 133 | ajax_wrong_port_factory = function(name) { 134 | var port, _i, _len, _ref, _results; 135 | _ref = [25, 8999, 65300]; 136 | _results = []; 137 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 138 | port = _ref[_i]; 139 | _results.push(asyncTest(name + ' wrong port ' + port, function() { 140 | return test_wrong_url(name, 'http://localhost:' + port + '/wrong_url_indeed.txt', [0]); 141 | })); 142 | } 143 | return _results; 144 | }; 145 | 146 | ajax_simple_factory('XHRLocalObject'); 147 | 148 | if (window.XDomainRequest) ajax_simple_factory('XDRObject'); 149 | 150 | if (!window.ActiveXObject) ajax_streaming_factory('XHRLocalObject'); 151 | 152 | if (window.XDomainRequest) ajax_streaming_factory('XDRObject'); 153 | 154 | ajax_wrong_port_factory('XHRLocalObject'); 155 | 156 | if (window.XDomainRequest) ajax_wrong_port_factory('XDRObject'); 157 | 158 | asyncTest('XHRLocalObject wrong url', function() { 159 | return test_wrong_url('XHRLocalObject', '/wrong_url_indeed.txt', [0, 404]); 160 | }); 161 | 162 | if (window.XDomainRequest) { 163 | asyncTest('XDRObject wrong url', function() { 164 | return test_wrong_url('XDRObject', '/wrong_url_indeed.txt', [0]); 165 | }); 166 | } 167 | -------------------------------------------------------------------------------- /qunit/server.py: -------------------------------------------------------------------------------- 1 | from twisted.internet import reactor, protocol, ssl 2 | from twisted.web import static, server, resource 3 | from OpenSSL import SSL 4 | from txsockjs.factory import SockJSResource 5 | 6 | SECURE = True 7 | 8 | ### The website 9 | 10 | class Config(resource.Resource): 11 | isLeaf = True 12 | def render_GET(self, request): 13 | request.setHeader('content-type', 'application/javascript; charset=UTF-8') 14 | return """var client_opts = {{ 15 | // Address of a sockjs test server. 16 | url: 'http{}://irc.fugiman.com:8081', 17 | sockjs_opts: {{ 18 | devel: true, 19 | debug: true, 20 | info: {{cookie_needed:false}} 21 | }} 22 | }};""".format("s" if SECURE else "") 23 | 24 | class SlowScript(resource.Resource): 25 | isLeaf = True 26 | def render_GET(self, request): 27 | request.setHeader('content-type', 'application/javascript; charset=UTF-8') 28 | request.write("") 29 | reactor.callLater(0.500, self.done, request) 30 | return server.NOT_DONE_YET 31 | 32 | def done(self, request): 33 | request.write("var a = 1;\n") 34 | request.finish() 35 | 36 | class Streaming(resource.Resource): 37 | isLeaf = True 38 | def render_GET(self, request): 39 | request.setHeader('content-type', 'text/plain; charset=UTF-8') 40 | request.setHeader('Access-Control-Allow-Origin', '*') 41 | request.write("a"*2048+"\n") 42 | reactor.callLater(0.250, self.done, request) 43 | return server.NOT_DONE_YET 44 | 45 | def done(self, request): 46 | request.write("b\n") 47 | request.finish() 48 | 49 | class Simple(resource.Resource): 50 | isLeaf = True 51 | def render_GET(self, request): 52 | request.setHeader('content-type', 'text/plain; charset=UTF-8') 53 | request.setHeader('Access-Control-Allow-Origin', '*') 54 | return "a"*2048+"\nb\n" 55 | 56 | class WrongURL(resource.Resource): 57 | isLeaf = True 58 | def render_GET(self, request): 59 | request.setResponseCode(404) 60 | return "" 61 | 62 | website_root = static.File("qunit/html") 63 | website_root.putChild("slow-script.js", SlowScript()) 64 | website_root.putChild("streaming.txt", Streaming()) 65 | website_root.putChild("simple.txt", Simple()) 66 | website_root.putChild("wrong_url_indeed.txt", WrongURL()) 67 | website_root.putChild("config.js", Config()) 68 | website = server.Site(website_root) 69 | reactor.listenTCP(8082, website) 70 | 71 | ### The SockJS server 72 | 73 | class Echo(protocol.Protocol): 74 | def dataReceived(self, data): 75 | self.transport.write(data) 76 | 77 | class EchoFactory(protocol.Factory): 78 | protocol = Echo 79 | 80 | class Close(protocol.Protocol): 81 | def connectionMade(self): 82 | self.transport.loseConnection() 83 | 84 | class CloseFactory(protocol.Factory): 85 | protocol = Close 86 | 87 | class Ticker(protocol.Protocol): 88 | ticker = None 89 | def connectionMade(self): 90 | self.ticker = reactor.callLater(1, self.tick) 91 | 92 | def tick(self): 93 | self.transport.write("tick!") 94 | self.ticker = reactor.callLater(1, self.tick) 95 | 96 | def connectionLost(self, reason=None): 97 | if self.ticker: 98 | self.ticker.cancel() 99 | 100 | class TickerFactory(protocol.Factory): 101 | protocol = Ticker 102 | 103 | class Amplify(protocol.Protocol): 104 | def dataReceived(self, data): 105 | length = int(data) 106 | length = length if length > 0 and length < 19 else 1 107 | self.transport.write("x" * 2**length) 108 | 109 | class AmplifyFactory(protocol.Factory): 110 | protocol = Amplify 111 | 112 | class Broadcast(protocol.Protocol): 113 | def connectionMade(self): 114 | self.factory.connections[self] = 1 115 | 116 | def dataReceived(self, data): 117 | for p in self.factory.connections.keys(): 118 | p.transport.write(data) 119 | 120 | def connectionLost(self, reason=None): 121 | del self.factory.connections[self] 122 | 123 | class BroadcastFactory(protocol.Factory): 124 | protocol = Broadcast 125 | connections = {} 126 | 127 | echo = EchoFactory() 128 | close = CloseFactory() 129 | ticker = TickerFactory() 130 | amplify = AmplifyFactory() 131 | broadcast = BroadcastFactory() 132 | 133 | sockjs_root = resource.Resource() 134 | sockjs_root.putChild("echo", SockJSResource(echo, {'streaming_limit': 4 * 1024})) 135 | sockjs_root.putChild("disabled_websocket_echo", SockJSResource(echo, {'websocket': False})) 136 | sockjs_root.putChild("cookie_needed_echo", SockJSResource(echo, {'cookie_needed': True})) 137 | sockjs_root.putChild("close", SockJSResource(close)) 138 | sockjs_root.putChild("ticker", SockJSResource(ticker)) 139 | sockjs_root.putChild("amplify", SockJSResource(amplify)) 140 | sockjs_root.putChild("broadcast", SockJSResource(broadcast)) 141 | sockjs = server.Site(sockjs_root) 142 | 143 | 144 | ### SSL shenanigans 145 | 146 | # A direct copy of DefaultOpenSSLContextFactory as of Twisted 12.2.0 147 | # The only difference is using ctx.use_certificate_chain_file instead of ctx.use_certificate_file 148 | class ChainedOpenSSLContextFactory(ssl.DefaultOpenSSLContextFactory): 149 | def cacheContext(self): 150 | if self._context is None: 151 | ctx = self._contextFactory(self.sslmethod) 152 | ctx.set_options(SSL.OP_NO_SSLv2) 153 | ctx.use_certificate_chain_file(self.certificateFileName) 154 | ctx.use_privatekey_file(self.privateKeyFileName) 155 | self._context = ctx 156 | 157 | if SECURE: 158 | ssl_cert = ChainedOpenSSLContextFactory("ssl.key","ssl.pem") 159 | reactor.listenSSL(8081, sockjs, ssl_cert) 160 | else: 161 | reactor.listenTCP(8081, sockjs) 162 | 163 | ### Run the reactor 164 | 165 | reactor.run() 166 | -------------------------------------------------------------------------------- /txsockjs/factory.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012, Christopher Gamble 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the Christopher Gamble nor the names of its 12 | # contributors may be used to endorse or promote products derived 13 | # from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 19 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 20 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 23 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 24 | # OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | from twisted.web import resource, server 27 | from txsockjs.protocols.base import Stub 28 | from txsockjs.protocols.eventsource import EventSource 29 | from txsockjs.protocols.htmlfile import HTMLFile 30 | from txsockjs.protocols.jsonp import JSONP, JSONPSend 31 | from txsockjs.protocols.static import Info, IFrame 32 | from txsockjs.protocols.websocket import RawWebSocket, WebSocket 33 | from txsockjs.protocols.xhr import XHR, XHRSend, XHRStream 34 | 35 | class SockJSFactory(server.Site): 36 | def __init__(self, factory, options = None): 37 | server.Site.__init__(self, SockJSResource(factory, options)) 38 | 39 | class SockJSMultiFactory(server.Site): 40 | def __init__(self): 41 | server.Site.__init__(self, resource.Resource()) 42 | 43 | def addFactory(self, factory, prefix, options = None): 44 | self.resource.putChild(prefix, SockJSResource(factory, options)) 45 | 46 | class SockJSResource(resource.Resource): 47 | def __init__(self, factory, options = None): 48 | resource.Resource.__init__(self) 49 | self._factory = factory 50 | self._sessions = {} 51 | self._options = { 52 | 'websocket': True, 53 | 'cookie_needed': False, 54 | 'heartbeat': 25, 55 | 'timeout': 5, 56 | 'streaming_limit': 128 * 1024, 57 | 'encoding': 'cp1252', #Latin1 58 | 'sockjs_url': 'https://d1fxtkz8shb9d2.cloudfront.net/sockjs-0.3.js', 59 | 'proxy_header': None 60 | } 61 | if options is not None: 62 | self._options.update(options) 63 | # Just in case somebody wants to mess with these 64 | self._methods = { 65 | 'xhr': XHR, 66 | 'xhr_send': XHRSend, 67 | 'xhr_streaming': XHRStream, 68 | 'eventsource': EventSource, 69 | 'htmlfile': HTMLFile, 70 | 'jsonp': JSONP, 71 | 'jsonp_send': JSONPSend, 72 | } 73 | self._writeMethods = ('xhr_send','jsonp_send') 74 | # Static Resources 75 | self.putChild("info",Info()) 76 | self.putChild("iframe.html",IFrame()) 77 | self.putChild("websocket",RawWebSocket()) 78 | # Since it's constant, we can declare the websocket handler up here 79 | self._websocket = WebSocket() 80 | self._websocket.parent = self 81 | 82 | def getChild(self, name, request): 83 | # Check if it is the greeting url 84 | if not name and not request.postpath: 85 | return self 86 | # Hacks to resove the iframe even when people are dumb 87 | if len(name) > 10 and name[:6] == "iframe" and name[-5:] == ".html": 88 | return self.children["iframe.html"] 89 | # Sessions must have 3 parts, name is already the first. Also, no periods in the loadbalancer 90 | if len(request.postpath) != 2 or "." in name or not name: 91 | return resource.NoResource("No such child resource.") 92 | # Extract session & request type. Discard load balancer 93 | session, name = request.postpath 94 | # No periods in the session 95 | if "." in session or not session: 96 | return resource.NoResource("No such child resource.") 97 | # Websockets are a special case 98 | if name == "websocket": 99 | return self._websocket 100 | # Reject invalid methods 101 | if name not in self._methods: 102 | return resource.NoResource("No such child resource.") 103 | # Reject writes to invalid sessions, unless just checking options 104 | if name in self._writeMethods and session not in self._sessions and request.method != "OPTIONS": 105 | return resource.NoResource("No such child resource.") 106 | # Generate session if doesn't exist, unless just checking options 107 | if session not in self._sessions and request.method != "OPTIONS": 108 | self._sessions[session] = Stub(self, session) 109 | # Delegate request to appropriate handler 110 | return self._methods[name](self, self._sessions[session] if request.method != "OPTIONS" else None) 111 | 112 | def putChild(self, path, child): 113 | child.parent = self 114 | resource.Resource.putChild(self, path, child) 115 | 116 | def setBaseHeaders(self, request, cookie=True): 117 | origin = request.getHeader("Origin") 118 | headers = request.getHeader('Access-Control-Request-Headers') 119 | if origin is None or origin == "null": 120 | origin = "*" 121 | request.setHeader('access-control-allow-origin', origin) 122 | request.setHeader('access-control-allow-credentials', 'true') 123 | request.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') 124 | if headers is not None: 125 | request.setHeader('Access-Control-Allow-Headers', headers) 126 | if self._options["cookie_needed"] and cookie: 127 | cookie = request.getCookie("JSESSIONID") if request.getCookie("JSESSIONID") else "dummy" 128 | request.addCookie("JSESSIONID", cookie, path="/") 129 | 130 | def render_GET(self, request): 131 | self.setBaseHeaders(request,False) 132 | request.setHeader('content-type', 'text/plain; charset=UTF-8') 133 | return "Welcome to SockJS!\n" 134 | -------------------------------------------------------------------------------- /txsockjs/protocols/websocket.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012, Christopher Gamble 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the Christopher Gamble nor the names of its 12 | # contributors may be used to endorse or promote products derived 13 | # from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 19 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 20 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 23 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 24 | # OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | try: 27 | from twisted.web.websockets import WebSocketsResource 28 | except ImportError: 29 | from txsockjs.websockets import WebSocketsResource 30 | 31 | from zope.interface import directlyProvides, providedBy 32 | from twisted.internet import reactor, address 33 | from twisted.internet.protocol import Protocol 34 | from twisted.protocols.policies import WrappingFactory, ProtocolWrapper 35 | from twisted.web.server import NOT_DONE_YET 36 | from txsockjs.oldwebsockets import OldWebSocketsResource 37 | from txsockjs.utils import normalize 38 | import json, re 39 | 40 | 41 | class PeerOverrideProtocol(ProtocolWrapper): 42 | def getPeer(self): 43 | if self.parent._options["proxy_header"] and self.request.requestHeaders.hasHeader(self.parent._options["proxy_header"]): 44 | ip = self.request.requestHeaders.getRawHeaders(self.parent._options["proxy_header"])[0].split(",")[-1].strip() 45 | if re.match("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", ip): 46 | return address.IPv4Address("TCP", ip, None) 47 | else: 48 | return address.IPv6Address("TCP", ip, None) 49 | return ProtocolWrapper.getPeer(self) 50 | 51 | class JsonProtocol(PeerOverrideProtocol): 52 | def makeConnection(self, transport): 53 | directlyProvides(self, providedBy(transport)) 54 | Protocol.makeConnection(self, transport) 55 | self.transport.write("o") 56 | self.factory.registerProtocol(self) 57 | self.wrappedProtocol.makeConnection(self) 58 | self.heartbeat_timer = reactor.callLater(self.parent._options['heartbeat'], self.heartbeat) 59 | 60 | def write(self, data): 61 | self.writeSequence([data]) 62 | 63 | def writeSequence(self, data): 64 | data = list(data) 65 | for index, p in enumerate(data): 66 | data[index] = normalize(p, self.parent._options['encoding']) 67 | self.transport.write("a{0}".format(json.dumps(data, separators=(',',':')))) 68 | 69 | def writeRaw(self, data): 70 | self.transport.write(data) 71 | 72 | def loseConnection(self): 73 | self.transport.write('c[3000,"Go away!"]') 74 | ProtocolWrapper.loseConnection(self) 75 | 76 | def connectionLost(self, reason=None): 77 | if self.heartbeat_timer.active(): 78 | self.heartbeat_timer.cancel() 79 | PeerOverrideProtocol.connectionLost(self, reason) 80 | 81 | def dataReceived(self, data): 82 | if not data: 83 | return 84 | try: 85 | dat = json.loads(data) 86 | except ValueError: 87 | self.transport.loseConnection() 88 | else: 89 | for d in dat: 90 | d = normalize(d, self.parent._options['encoding']) 91 | ProtocolWrapper.dataReceived(self, d) 92 | 93 | def heartbeat(self): 94 | self.transport.write('h') 95 | self.heartbeat_timer = reactor.callLater(self.parent._options['heartbeat'], self.heartbeat) 96 | 97 | class PeerOverrideFactory(WrappingFactory): 98 | protocol = PeerOverrideProtocol 99 | 100 | class JsonFactory(WrappingFactory): 101 | protocol = JsonProtocol 102 | 103 | class RawWebSocket(WebSocketsResource, OldWebSocketsResource): 104 | def __init__(self): 105 | self._factory = None 106 | 107 | def _makeFactory(self): 108 | f = PeerOverrideFactory(self.parent._factory) 109 | WebSocketsResource.__init__(self, self.parent._factory) 110 | OldWebSocketsResource.__init__(self, self.parent._factory) 111 | 112 | def lookupProtocol(self, protocolNames, request, old = False): 113 | if old: 114 | protocol = self._oldfactory.buildProtocol(request.transport.getPeer()) 115 | else: 116 | protocol = self._factory.buildProtocol(request.transport.getPeer()) 117 | protocol.request = request 118 | protocol.parent = self.parent 119 | return protocol, None 120 | 121 | def render(self, request): 122 | # Get around .parent limitation 123 | if self._factory is None: 124 | self._makeFactory() 125 | # Override handling of invalid methods, returning 400 makes SockJS mad 126 | if request.method != 'GET': 127 | request.setResponseCode(405) 128 | request.defaultContentType = None # SockJS wants this gone 129 | request.setHeader('Allow','GET') 130 | return "" 131 | # Override handling of lack of headers, again SockJS requires non-RFC stuff 132 | upgrade = request.getHeader("Upgrade") 133 | if upgrade is None or "websocket" not in upgrade.lower(): 134 | request.setResponseCode(400) 135 | return 'Can "Upgrade" only to "WebSocket".' 136 | connection = request.getHeader("Connection") 137 | if connection is None or "upgrade" not in connection.lower(): 138 | request.setResponseCode(400) 139 | return '"Connection" must be "Upgrade".' 140 | # Defer to inherited methods 141 | ret = WebSocketsResource.render(self, request) # For RFC versions of websockets 142 | if ret is NOT_DONE_YET: 143 | return ret 144 | return OldWebSocketsResource.render(self, request) # For non-RFC versions of websockets 145 | 146 | class WebSocket(RawWebSocket): 147 | def _makeFactory(self): 148 | f = JsonFactory(self.parent._factory) 149 | WebSocketsResource.__init__(self, f) 150 | OldWebSocketsResource.__init__(self, f) 151 | -------------------------------------------------------------------------------- /txsockjs/protocols/base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012, Christopher Gamble 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the Christopher Gamble nor the names of its 12 | # contributors may be used to endorse or promote products derived 13 | # from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 19 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 20 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 23 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 24 | # OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | from zope.interface import directlyProvides, providedBy 27 | from twisted.internet import reactor, protocol, address 28 | from twisted.web import resource, server, http 29 | from twisted.protocols.policies import ProtocolWrapper 30 | from txsockjs.utils import normalize 31 | import json, re 32 | 33 | class StubResource(resource.Resource, ProtocolWrapper): 34 | isLeaf = True 35 | def __init__(self, parent, session): 36 | resource.Resource.__init__(self) 37 | ProtocolWrapper.__init__(self, None, session) 38 | self.parent = parent 39 | self.session = session 40 | self.putChild("", self) 41 | 42 | def render_OPTIONS(self, request): 43 | method = "POST" if getattr(self, "render_POST", None) is not None else "GET" 44 | request.setResponseCode(http.NO_CONTENT) 45 | self.parent.setBaseHeaders(request,False) 46 | request.setHeader('Cache-Control', 'public, max-age=31536000') 47 | request.setHeader('access-control-max-age', '31536000') 48 | request.setHeader('Expires', 'Fri, 01 Jan 2500 00:00:00 GMT') #Get a new library by then 49 | request.setHeader('Access-Control-Allow-Methods', 'OPTIONS, {0}'.format(method)) # Hardcoding this may be bad? 50 | return "" 51 | 52 | def connect(self, request): 53 | if self.session.attached: 54 | return 'c[2010,"Another connection still open"]\n' 55 | self.request = request 56 | directlyProvides(self, providedBy(request.transport)) 57 | protocol.Protocol.makeConnection(self, request.transport) 58 | self.session.makeConnection(self) 59 | request.notifyFinish().addErrback(self.connectionLost) 60 | return server.NOT_DONE_YET 61 | 62 | def disconnect(self): 63 | self.request.finish() 64 | self.session.transportLeft() 65 | 66 | def loseConnection(self): 67 | self.request.finish() 68 | self.session.transportLeft() 69 | 70 | def connectionLost(self, reason=None): 71 | self.wrappedProtocol.connectionLost(reason) 72 | 73 | def getPeer(self): 74 | if self.parent._options["proxy_header"] and self.request.requestHeaders.hasHeader(self.parent._options["proxy_header"]): 75 | ip = self.request.requestHeaders.getRawHeaders(self.parent._options["proxy_header"])[0].split(",")[-1].strip() 76 | if re.match("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", ip): 77 | return address.IPv4Address("TCP", ip, None) 78 | else: 79 | return address.IPv6Address("TCP", ip, None) 80 | return ProtocolWrapper.getPeer(self) 81 | 82 | 83 | class Stub(ProtocolWrapper): 84 | def __init__(self, parent, session): 85 | self.parent = parent 86 | self.session = session 87 | self.pending = [] 88 | self.buffer = [] 89 | self.connecting = True 90 | self.disconnecting = False 91 | self.attached = False 92 | self.transport = None # Upstream (SockJS) 93 | self.protocol = None # Downstream (Wrapped Factory) 94 | self.peer = None 95 | self.host = None 96 | self.timeout = reactor.callLater(self.parent._options['timeout'], self.disconnect) 97 | self.heartbeat_timer = reactor.callLater(self.parent._options['heartbeat'], self.heartbeat) 98 | 99 | def makeConnection(self, transport): 100 | directlyProvides(self, providedBy(transport)) 101 | protocol.Protocol.makeConnection(self, transport) 102 | self.attached = True 103 | self.peer = self.transport.getPeer() 104 | self.host = self.transport.getHost() 105 | if self.timeout.active(): 106 | self.timeout.cancel() 107 | if self.protocol is None: 108 | self.protocol = self.parent._factory.buildProtocol(self.transport.getPeer()) 109 | if self.protocol is None: 110 | self.connectionLost() 111 | else: 112 | self.protocol.makeConnection(self) 113 | self.sendData() 114 | 115 | def loseConnection(self): 116 | self.disconnecting = True 117 | self.sendData() 118 | 119 | def connectionLost(self, reason=None): 120 | if self.attached: 121 | self.disconnecting = True 122 | self.transport = None 123 | self.attached = False 124 | self.disconnect() 125 | 126 | def heartbeat(self): 127 | self.pending.append('h') 128 | self.heartbeat_timer = reactor.callLater(self.parent._options['heartbeat'], self.heartbeat) 129 | self.sendData() 130 | 131 | def disconnect(self): 132 | if self.protocol: 133 | self.protocol.connectionLost(None) 134 | del self.parent._sessions[self.session] 135 | if self.timeout.active(): 136 | self.timeout.cancel() 137 | if self.heartbeat_timer.active(): 138 | self.heartbeat_timer.cancel() 139 | 140 | def transportLeft(self): 141 | self.transport = None 142 | self.attached = False 143 | self.timeout = reactor.callLater(self.parent._options['timeout'], self.disconnect) 144 | 145 | def write(self, data): 146 | data = normalize(data, self.parent._options['encoding']) 147 | self.buffer.append(data) 148 | self.sendData() 149 | 150 | def writeSequence(self, data): 151 | data = list(data) 152 | for index, p in enumerate(data): 153 | data[index] = normalize(p, self.parent._options['encoding']) 154 | self.buffer.extend(data) 155 | self.sendData() 156 | 157 | def writeRaw(self, data): 158 | self.flushData() 159 | self.pending.append(data) 160 | self.sendData() 161 | 162 | def sendData(self): 163 | if self.transport: 164 | if self.connecting: 165 | self.transport.write('o') 166 | self.connecting = False 167 | self.sendData() 168 | elif self.disconnecting: 169 | self.transport.write('c[3000,"Go away!"]') 170 | if self.transport: 171 | self.transport.loseConnection() 172 | else: 173 | self.flushData() 174 | if self.pending: 175 | data = list(self.pending) 176 | self.pending = [] 177 | self.transport.writeSequence(data) 178 | 179 | def flushData(self): 180 | if self.buffer: 181 | data = 'a{0}'.format(json.dumps(self.buffer, separators=(',',':'))) 182 | self.buffer = [] 183 | self.pending.append(data) 184 | 185 | def requeue(self, data): 186 | data.extend(self.pending) 187 | self.pending = data 188 | 189 | def dataReceived(self, data): 190 | if self.timeout.active(): 191 | self.timeout.reset(5) 192 | if data == '': 193 | return "Payload expected." 194 | try: 195 | packets = json.loads(data) 196 | for p in packets: 197 | p = normalize(p, self.parent._options['encoding']) 198 | if self.protocol: 199 | self.protocol.dataReceived(p) 200 | return None 201 | except ValueError: 202 | return "Broken JSON encoding." 203 | 204 | def getPeer(self): 205 | return self.peer 206 | 207 | def getHost(self): 208 | return self.host 209 | 210 | def registerProducer(self, producer, streaming): 211 | if self.transport: 212 | self.transport.registerProducer(producer, streaming) 213 | 214 | def unregisterProducer(self): 215 | if self.transport: 216 | self.transport.unregisterProducer() 217 | 218 | def stopConsuming(self): 219 | if self.transport: 220 | self.transport.stopConsuming() 221 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | SockJS-Twisted 3 | ============== 4 | 5 | A simple library for adding SockJS support to your twisted application. 6 | 7 | Status 8 | ====== 9 | 10 | SockJS-Twisted passes all `SockJS-Protocol v0.3.3 `_ tests, 11 | and all `SockJS-Client qunit `_ tests. It has been used in 12 | production environments, and should be free of any critical bugs. 13 | 14 | Usage 15 | ===== 16 | 17 | Use ``txsockjs.factory.SockJSFactory`` to wrap your factories. That's it! 18 | 19 | .. code-block:: python 20 | 21 | from twisted.internet import reactor 22 | from twisted.internet.protocol import Factory, Protocol 23 | from txsockjs.factory import SockJSFactory 24 | 25 | class HelloProtocol(Protocol): 26 | def connectionMade(self): 27 | self.transport.write('hello') 28 | self.transport.write('how are you?') 29 | 30 | def dataReceived(self, data): 31 | print data 32 | 33 | reactor.listenTCP(8080, SockJSFactory(Factory.forProtocol(HelloProtocol))) 34 | reactor.run() 35 | 36 | There is nothing else to it, no special setup involved. 37 | 38 | Do you want a secure connection? Use ``listenSSL()`` instead of ``listenTCP()``. 39 | 40 | Advanced Usage 41 | ============== 42 | 43 | For those who want to host multiple SockJS services off of one port, 44 | ``txsockjs.factory.SockJSMultiFactory`` is designed to handle routing for you. 45 | 46 | .. code-block:: python 47 | 48 | from twisted.internet import reactor 49 | from twisted.internet.protocol import Factory, Protocol 50 | from txsockjs.factory import SockJSMultiFactory 51 | from txsockjs.utils import broadcast 52 | 53 | class EchoProtocol(Protocol): 54 | def dataReceived(self, data): 55 | self.transport.write(data) 56 | 57 | class ChatProtocol(Protocol): 58 | def connectionMade(self): 59 | if not hasattr(self.factory, "transports"): 60 | self.factory.transports = set() 61 | self.factory.transports.add(self.transport) 62 | 63 | def dataReceived(self, data): 64 | broadcast(data, self.factory.transports) 65 | 66 | def connectionLost(self, reason): 67 | self.factory.transports.remove(self.transport) 68 | 69 | f = SockJSMultiFactory() 70 | f.addFactory(Factory.forProtocol(EchoProtocol), "echo") 71 | f.addFactory(Factory.forProtocol(ChatProtocol), "chat") 72 | 73 | reactor.listenTCP(8080, f) 74 | reactor.run() 75 | 76 | http://localhost:8080/echo and http://localhost:8080/chat will give you access 77 | to your EchoFactory and ChatFactory. 78 | 79 | Integration With Websites 80 | ========================= 81 | 82 | It is possible to offer static resources, dynamic pages, and SockJS endpoints off of 83 | a single port by using ``txsockjs.factory.SockJSResource``. 84 | 85 | .. code-block:: python 86 | 87 | from twisted.internet import reactor 88 | from twisted.internet.protocol import Factory, Protocol 89 | from twisted.web import resource, server 90 | from txsockjs.factory import SockJSResource 91 | 92 | # EchoProtocol and ChatProtocol defined above 93 | 94 | root = resource.Resource() 95 | root.putChild("echo", SockJSResource(Factory.forProtocol(EchoProtocol))) 96 | root.putChild("chat", SockJSResource(Factory.forProtocol(ChatProtocol))) 97 | site = server.Site(root) 98 | 99 | reactor.listenTCP(8080, site) 100 | reactor.run() 101 | 102 | Multiplexing [Experimental] 103 | =========================== 104 | 105 | SockJS-Twisted also has built-in support for multiplexing. See the 106 | `Websocket-Multiplex `_ library 107 | for how to integrate multiplexing client side. 108 | 109 | .. code-block:: python 110 | 111 | from twisted.internet import reactor 112 | from twisted.internet.protocol import Factory, Protocol 113 | from twisted.web import resource, server 114 | from txsockjs.multiplex import SockJSMultiplexResource 115 | 116 | multiplex = SockJSMultiplexResource() 117 | multiplex.addFactory("echo", Factory.forProtocol(EchoProtocol)) 118 | multiplex.addFactory("chat", Factory.forProtocol(ChatProtocol)) 119 | 120 | root = resource.Resource() 121 | root.putChild("multiplex", multiplex) 122 | site = server.Site(root) 123 | 124 | reactor.listenTCP(8080, site) 125 | reactor.run() 126 | 127 | Single factory? Multifactory? Resource? Multiplexing? What's the difference? 128 | ============================================================================ 129 | 130 | +-------------------------+--------------------+----------------------------------+--------------------------+ 131 | | Type | Factories per port | Allows mixing native web content | Factories per connection | 132 | +=========================+====================+==================================+==========================+ 133 | | SockJSFactory | Single | No | Single | 134 | +-------------------------+--------------------+----------------------------------+--------------------------+ 135 | | SockJSMultiFactory | Multiple | No | Single | 136 | +-------------------------+--------------------+----------------------------------+--------------------------+ 137 | | SockJSResource | Multiple | Yes | Single | 138 | +-------------------------+--------------------+----------------------------------+--------------------------+ 139 | | SockJSMultiplexResource | Multiple | Yes | Multiple | 140 | +-------------------------+--------------------+----------------------------------+--------------------------+ 141 | 142 | ``SockJSFactory`` is recommended for use in non-web (HTTP) applications to allow 143 | native web connections. For instance, an IRC server. There can only be one factory 144 | listening on a port using this method. The SockJS endpoint uses this internally. 145 | 146 | ``SockJSMultiFactory`` is recommended for use in non-web (HTTP) applications with 147 | multiple services. This allows multiple factories to listen on a single port. 148 | 149 | ``SockJSResource`` is recommended for use in HTTP based applications, like webservers. 150 | 151 | ``SockJSMultiplexResource`` is recommended for pubsub applications, where each connection 152 | needs to talk to multiple factories. Overriding the subscribe method allows for dynamic 153 | factory creation if you don't know what is needed server-side ahead of time. 154 | 155 | Endpoints 156 | ========= 157 | 158 | For integration with pre-existing libraries or programs, it is possible use sockjs 159 | as an endpoint in the form ``sockjs:tcp\:9090\:interface\=0.0.0.0:encoding=utf8:websocket=false``. 160 | You can pass any escaped endpoint to a sockjs endpoint to wrap it with txsockjs, and you can 161 | specify any option for the SockJSFactory by specifying it as a keyword argument. 162 | For more information, read the 163 | `twisted documentation on endpoints `_. 164 | 165 | .. code-block:: python 166 | 167 | from twisted.internet import reactor 168 | from twisted.internet.protocol import Factory, Protocol 169 | from twisted.internet.endpoints import serverFromString 170 | # Note that we don't have to import anything from txsockjs 171 | 172 | # HelloProtocol defined above 173 | 174 | endpoint = serverFromString(reactor, "sockjs:tcp\:8080") 175 | endpoint.listen(Factory.forProtocol(HelloProtocol)) 176 | reactor.run() 177 | 178 | Options 179 | ======= 180 | 181 | A dictionary of options can be passed into the factory to control SockJS behavior. 182 | 183 | .. code-block:: python 184 | 185 | options = { 186 | 'websocket': True, 187 | 'cookie_needed': False, 188 | 'heartbeat': 25, 189 | 'timeout': 5, 190 | 'streaming_limit': 128 * 1024, 191 | 'encoding': 'cp1252', # Latin1 192 | 'sockjs_url': 'https://d1fxtkz8shb9d2.cloudfront.net/sockjs-0.3.js', 193 | 'proxy_header': None 194 | } 195 | SockJSFactory(factory_to_wrap, options) 196 | SockJSMultiFactory().addFactory(factory_to_wrap, prefix, options) 197 | SockJSResource(factory_to_wrap, options) 198 | SockJSMultiplexResource(options) 199 | 200 | websocket : 201 | whether websockets are supported as a protocol. Useful for proxies or load balancers that don't support websockets. 202 | 203 | cookie_needed : 204 | whether the JSESSIONID cookie is set. Results in less performant protocols being used, so don't require them unless your load balancer requires it. 205 | 206 | heartbeat : 207 | how often a heartbeat message is sent to keep the connection open. Do not increase this unless you know what you are doing. 208 | 209 | timeout : 210 | maximum delay between connections before the underlying protocol is disconnected 211 | 212 | streaming_limit : 213 | how many bytes can be sent over a streaming protocol before it is cycled. Allows browser-side garbage collection to lower RAM usage. 214 | 215 | encoding : 216 | All messages to and from txsockjs should be valid UTF-8. In the event that a message received by txsockjs is not UTF-8, fall back to this encoding. 217 | 218 | sockjs_url : 219 | The url of the SockJS library to use in iframes. By default this is served over HTTPS and therefore shouldn't need changing. 220 | 221 | proxy_header : 222 | The HTTP header to pull a proxied IP address out of. Leave as None to get the unproxied IP. **Do not change this unless you are behind a proxy you control.** 223 | 224 | License 225 | ======= 226 | 227 | SockJS-Twisted is (c) 2012 Christopher Gamble and is made available under the BSD license. 228 | -------------------------------------------------------------------------------- /qunit/html/lib/unittests.js: -------------------------------------------------------------------------------- 1 | var u; 2 | 3 | module('Utils'); 4 | 5 | u = SockJS.getUtils(); 6 | 7 | test('random_string', function() { 8 | var i, _i, _len, _ref; 9 | notEqual(u.random_string(8), u.random_string(8)); 10 | _ref = [1, 2, 3, 128]; 11 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 12 | i = _ref[_i]; 13 | equal(u.random_string(i).length, i); 14 | } 15 | return equal(u.random_string(4, 1), 'aaaa'); 16 | }); 17 | 18 | test('random_number_string', function() { 19 | var i, _results; 20 | _results = []; 21 | for (i = 0; i <= 10; i++) { 22 | equal(u.random_number_string(10).length, 1); 23 | equal(u.random_number_string(100).length, 2); 24 | equal(u.random_number_string(1000).length, 3); 25 | equal(u.random_number_string(10000).length, 4); 26 | _results.push(equal(u.random_number_string(100000).length, 5)); 27 | } 28 | return _results; 29 | }); 30 | 31 | test('getOrigin', function() { 32 | equal(u.getOrigin('http://a.b/'), 'http://a.b'); 33 | equal(u.getOrigin('http://a.b/c'), 'http://a.b'); 34 | return equal(u.getOrigin('http://a.b:123/c'), 'http://a.b:123'); 35 | }); 36 | 37 | test('isSameOriginUrl', function() { 38 | ok(u.isSameOriginUrl('http://localhost', 'http://localhost/')); 39 | ok(u.isSameOriginUrl('http://localhost', 'http://localhost/abc')); 40 | ok(u.isSameOriginUrl('http://localhost/', 'http://localhost')); 41 | ok(u.isSameOriginUrl('http://localhost', 'http://localhost')); 42 | ok(u.isSameOriginUrl('http://localhost', 'http://localhost:8080') === false); 43 | ok(u.isSameOriginUrl('http://localhost:8080', 'http://localhost') === false); 44 | ok(u.isSameOriginUrl('http://localhost:8080', 'http://localhost:8080/')); 45 | ok(u.isSameOriginUrl('http://127.0.0.1:80/', 'http://127.0.0.1:80/a')); 46 | ok(u.isSameOriginUrl('http://127.0.0.1:80', 'http://127.0.0.1:80/a')); 47 | ok(u.isSameOriginUrl('http://localhost', 'http://localhost:80') === false); 48 | ok(u.isSameOriginUrl('http://127.0.0.1/', 'http://127.0.0.1:80/a') === false); 49 | ok(u.isSameOriginUrl('http://127.0.0.1:9', 'http://127.0.0.1:9999') === false); 50 | ok(u.isSameOriginUrl('http://127.0.0.1:99', 'http://127.0.0.1:9999') === false); 51 | ok(u.isSameOriginUrl('http://127.0.0.1:999', 'http://127.0.0.1:9999') === false); 52 | ok(u.isSameOriginUrl('http://127.0.0.1:9999', 'http://127.0.0.1:9999')); 53 | return ok(u.isSameOriginUrl('http://127.0.0.1:99999', 'http://127.0.0.1:9999') === false); 54 | }); 55 | 56 | test("getParentDomain", function() { 57 | var domains, k, _results; 58 | domains = { 59 | 'localhost': 'localhost', 60 | '127.0.0.1': '127.0.0.1', 61 | 'a.b.c.d': 'b.c.d', 62 | 'a.b.c.d.e': 'b.c.d.e', 63 | '[::1]': '[::1]', 64 | 'a.org': 'org', 65 | 'a2.a3.org': 'a3.org' 66 | }; 67 | _results = []; 68 | for (k in domains) { 69 | _results.push(equal(u.getParentDomain(k), domains[k])); 70 | } 71 | return _results; 72 | }); 73 | 74 | test('objectExtend', function() { 75 | var a, b; 76 | deepEqual(u.objectExtend({}, {}), {}); 77 | a = { 78 | a: 1 79 | }; 80 | equal(u.objectExtend(a, {}), a); 81 | equal(u.objectExtend(a, { 82 | b: 1 83 | }), a); 84 | a = { 85 | a: 1 86 | }; 87 | b = { 88 | b: 2 89 | }; 90 | deepEqual(u.objectExtend(a, b), { 91 | a: 1, 92 | b: 2 93 | }); 94 | deepEqual(a, { 95 | a: 1, 96 | b: 2 97 | }); 98 | return deepEqual(b, { 99 | b: 2 100 | }); 101 | }); 102 | 103 | test('bind', function() { 104 | var bound_fun, fun, o; 105 | o = {}; 106 | fun = function() { 107 | return this; 108 | }; 109 | deepEqual(fun(), window); 110 | bound_fun = u.bind(fun, o); 111 | return deepEqual(bound_fun(), o); 112 | }); 113 | 114 | test('amendUrl', function() { 115 | var dl, t; 116 | dl = document.location; 117 | equal(u.amendUrl('//blah:1/abc'), dl.protocol + '//blah:1/abc'); 118 | equal(u.amendUrl('/abc'), dl.protocol + '//' + dl.host + '/abc'); 119 | equal(u.amendUrl('/'), dl.protocol + '//' + dl.host); 120 | equal(u.amendUrl('http://a:1/abc'), 'http://a:1/abc'); 121 | equal(u.amendUrl('http://a:1/abc/'), 'http://a:1/abc'); 122 | equal(u.amendUrl('http://a:1/abc//'), 'http://a:1/abc'); 123 | t = function() { 124 | return u.amendUrl(''); 125 | }; 126 | raises(t, 'Wrong url'); 127 | t = function() { 128 | return u.amendUrl(false); 129 | }; 130 | raises(t, 'Wrong url'); 131 | t = function() { 132 | return u.amendUrl('http://abc?a=a'); 133 | }; 134 | raises(t, 'Only basic urls are supported'); 135 | t = function() { 136 | return u.amendUrl('http://abc#a'); 137 | }; 138 | return raises(t, 'Only basic urls are supported'); 139 | }); 140 | 141 | test('arrIndexOf', function() { 142 | var a; 143 | a = [1, 2, 3, 4, 5]; 144 | equal(u.arrIndexOf(a, 1), 0); 145 | equal(u.arrIndexOf(a, 5), 4); 146 | equal(u.arrIndexOf(a, null), -1); 147 | return equal(u.arrIndexOf(a, 6), -1); 148 | }); 149 | 150 | test('arrSkip', function() { 151 | var a; 152 | a = [1, 2, 3, 4, 5]; 153 | deepEqual(u.arrSkip(a, 1), [2, 3, 4, 5]); 154 | deepEqual(u.arrSkip(a, 2), [1, 3, 4, 5]); 155 | deepEqual(u.arrSkip(a, 11), [1, 2, 3, 4, 5]); 156 | deepEqual(u.arrSkip(a, 'a'), [1, 2, 3, 4, 5]); 157 | return deepEqual(u.arrSkip(a, '1'), [1, 2, 3, 4, 5]); 158 | }); 159 | 160 | test('quote', function() { 161 | var all_chars, c, i; 162 | equal(u.quote(''), '""'); 163 | equal(u.quote('a'), '"a"'); 164 | ok(u.arrIndexOf(['"\\t"', '"\\u0009"'], u.quote('\t')) !== -1); 165 | ok(u.arrIndexOf(['"\\n"', '"\\u000a"'], u.quote('\n')) !== -1); 166 | equal(u.quote('\x00\udfff\ufffe\uffff'), '"\\u0000\\udfff\\ufffe\\uffff"'); 167 | equal(u.quote('\ud85c\udff7\ud800\ud8ff'), '"\\ud85c\\udff7\\ud800\\ud8ff"'); 168 | equal(u.quote('\u2000\u2001\u0300\u0301'), '"\\u2000\\u2001\\u0300\\u0301"'); 169 | c = (function() { 170 | var _results; 171 | _results = []; 172 | for (i = 0; i <= 65535; i++) { 173 | _results.push(String.fromCharCode(i)); 174 | } 175 | return _results; 176 | })(); 177 | all_chars = c.join(''); 178 | return ok(JSON.parse(u.quote(all_chars)) === all_chars, "Quote/unquote all 64K chars."); 179 | }); 180 | 181 | test('detectProtocols', function() { 182 | var chrome_probed, ie10_probed, ie6_probed, ie8_probed, opera_probed; 183 | chrome_probed = { 184 | 'websocket': true, 185 | 'xdr-streaming': false, 186 | 'xhr-streaming': true, 187 | 'iframe-eventsource': true, 188 | 'iframe-htmlfile': true, 189 | 'xdr-polling': false, 190 | 'xhr-polling': true, 191 | 'iframe-xhr-polling': true, 192 | 'jsonp-polling': true 193 | }; 194 | deepEqual(u.detectProtocols(chrome_probed, null, {}), ['websocket', 'xhr-streaming', 'xhr-polling']); 195 | deepEqual(u.detectProtocols(chrome_probed, null, { 196 | websocket: false 197 | }), ['xhr-streaming', 'xhr-polling']); 198 | opera_probed = { 199 | 'websocket': false, 200 | 'xdr-streaming': false, 201 | 'xhr-streaming': false, 202 | 'iframe-eventsource': true, 203 | 'iframe-htmlfile': true, 204 | 'xdr-polling': false, 205 | 'xhr-polling': false, 206 | 'iframe-xhr-polling': true, 207 | 'jsonp-polling': true 208 | }; 209 | deepEqual(u.detectProtocols(opera_probed, null, {}), ['iframe-eventsource', 'iframe-xhr-polling']); 210 | ie6_probed = { 211 | 'websocket': false, 212 | 'xdr-streaming': false, 213 | 'xhr-streaming': false, 214 | 'iframe-eventsource': false, 215 | 'iframe-htmlfile': false, 216 | 'xdr-polling': false, 217 | 'xhr-polling': false, 218 | 'iframe-xhr-polling': false, 219 | 'jsonp-polling': true 220 | }; 221 | deepEqual(u.detectProtocols(ie6_probed, null, {}), ['jsonp-polling']); 222 | ie8_probed = { 223 | 'websocket': false, 224 | 'xdr-streaming': true, 225 | 'xhr-streaming': false, 226 | 'iframe-eventsource': false, 227 | 'iframe-htmlfile': true, 228 | 'xdr-polling': true, 229 | 'xhr-polling': false, 230 | 'iframe-xhr-polling': true, 231 | 'jsonp-polling': true 232 | }; 233 | deepEqual(u.detectProtocols(ie8_probed, null, {}), ['xdr-streaming', 'xdr-polling']); 234 | deepEqual(u.detectProtocols(ie8_probed, null, { 235 | cookie_needed: true 236 | }), ['iframe-htmlfile', 'iframe-xhr-polling']); 237 | ie10_probed = { 238 | 'websocket': true, 239 | 'xdr-streaming': true, 240 | 'xhr-streaming': true, 241 | 'iframe-eventsource': false, 242 | 'iframe-htmlfile': true, 243 | 'xdr-polling': true, 244 | 'xhr-polling': true, 245 | 'iframe-xhr-polling': true, 246 | 'jsonp-polling': true 247 | }; 248 | deepEqual(u.detectProtocols(ie10_probed, null, {}), ['websocket', 'xhr-streaming', 'xhr-polling']); 249 | deepEqual(u.detectProtocols(ie10_probed, null, { 250 | cookie_needed: true 251 | }), ['websocket', 'xhr-streaming', 'xhr-polling']); 252 | deepEqual(u.detectProtocols(chrome_probed, null, { 253 | null_origin: true 254 | }), ['websocket', 'iframe-eventsource', 'iframe-xhr-polling']); 255 | deepEqual(u.detectProtocols(chrome_probed, null, { 256 | websocket: false, 257 | null_origin: true 258 | }), ['iframe-eventsource', 'iframe-xhr-polling']); 259 | deepEqual(u.detectProtocols(opera_probed, null, { 260 | null_origin: true 261 | }), ['iframe-eventsource', 'iframe-xhr-polling']); 262 | deepEqual(u.detectProtocols(ie6_probed, null, { 263 | null_origin: true 264 | }), ['jsonp-polling']); 265 | deepEqual(u.detectProtocols(ie8_probed, null, { 266 | null_origin: true 267 | }), ['iframe-htmlfile', 'iframe-xhr-polling']); 268 | return deepEqual(u.detectProtocols(ie10_probed, null, { 269 | null_origin: true 270 | }), ['websocket', 'iframe-htmlfile', 'iframe-xhr-polling']); 271 | }); 272 | 273 | test("EventEmitter", function() { 274 | var bluff, r, single; 275 | expect(4); 276 | r = new SockJS('//1.2.3.4/wrongurl', null, { 277 | protocols_whitelist: [] 278 | }); 279 | r.addEventListener('message', function() { 280 | return ok(true); 281 | }); 282 | r.onmessage = function() { 283 | return ok(false); 284 | }; 285 | bluff = function() { 286 | return ok(false); 287 | }; 288 | r.addEventListener('message', bluff); 289 | r.removeEventListener('message', bluff); 290 | r.addEventListener('message', bluff); 291 | r.addEventListener('message', function() { 292 | return ok(true); 293 | }); 294 | r.onmessage = function() { 295 | return ok(true); 296 | }; 297 | r.removeEventListener('message', bluff); 298 | r.dispatchEvent({ 299 | type: 'message' 300 | }); 301 | single = function() { 302 | return ok(true); 303 | }; 304 | r.addEventListener('close', single); 305 | r.addEventListener('close', single); 306 | r.dispatchEvent({ 307 | type: 'close' 308 | }); 309 | r.removeEventListener('close', single); 310 | r.dispatchEvent({ 311 | type: 'close' 312 | }); 313 | return r.close(); 314 | }); 315 | -------------------------------------------------------------------------------- /qunit/html/lib/tests.js: -------------------------------------------------------------------------------- 1 | var arrIndexOf, batch_factory_factory, batch_factory_factory_amp, echo_factory_factory, escapable, factor_batch_large, factor_batch_large_amp, factor_echo_basic, factor_echo_large_message, factor_echo_rich, factor_echo_special_chars, factor_echo_unicode, factor_echo_utf_encoding, factor_echo_utf_encoding_simple, factor_server_close, factor_user_close, generate_killer_string, newSockJS, protocol, protocols, test_protocol_messages, _i, _len; 2 | 3 | protocols = ['websocket', 'xdr-streaming', 'xhr-streaming', 'iframe-eventsource', 'iframe-htmlfile', 'xdr-polling', 'xhr-polling', 'iframe-xhr-polling', 'jsonp-polling']; 4 | 5 | newSockJS = function(path, protocol) { 6 | var options, url; 7 | url = /^http/.test(path) ? path : client_opts.url + path; 8 | options = jQuery.extend({}, client_opts.sockjs_opts); 9 | if (protocol) options.protocols_whitelist = [protocol]; 10 | return new SockJS(url, null, options); 11 | }; 12 | 13 | echo_factory_factory = function(protocol, messages) { 14 | return function() { 15 | var a, r; 16 | expect(2 + messages.length); 17 | a = messages.slice(0); 18 | r = newSockJS('/echo', protocol); 19 | r.onopen = function(e) { 20 | ok(true); 21 | return r.send(a[0]); 22 | }; 23 | r.onmessage = function(e) { 24 | var i, x, xx1, xx2, _ref; 25 | x = '' + a[0]; 26 | if (e.data !== x) { 27 | for (i = 0, _ref = e.data.length; 0 <= _ref ? i < _ref : i > _ref; 0 <= _ref ? i++ : i--) { 28 | if (e.data.charCodeAt(i) !== x.charCodeAt(i)) { 29 | xx1 = ('0000' + x.charCodeAt(i).toString(16)).slice(-4); 30 | xx2 = ('0000' + e.data.charCodeAt(i).toString(16)).slice(-4); 31 | log('source: \\u' + xx1 + ' differs from: \\u' + xx2); 32 | break; 33 | } 34 | } 35 | } 36 | equal(e.data, '' + a[0]); 37 | a.shift(); 38 | if (typeof a[0] === 'undefined') { 39 | return r.close(); 40 | } else { 41 | return r.send(a[0]); 42 | } 43 | }; 44 | return r.onclose = function(e) { 45 | if (a.length) { 46 | ok(false, "Transport closed prematurely. " + e); 47 | } else { 48 | ok(true); 49 | } 50 | return start(); 51 | }; 52 | }; 53 | }; 54 | 55 | factor_echo_basic = function(protocol) { 56 | var messages; 57 | messages = ['data']; 58 | return echo_factory_factory(protocol, messages); 59 | }; 60 | 61 | factor_echo_rich = function(protocol) { 62 | var messages; 63 | messages = [ 64 | [1, 2, 3, 'data'], null, false, "data", 1, 12.0, { 65 | a: 1, 66 | b: 2 67 | } 68 | ]; 69 | return echo_factory_factory(protocol, messages); 70 | }; 71 | 72 | factor_echo_unicode = function(protocol) { 73 | var messages; 74 | messages = ["Τη γλώσσα μου έδωσαν ελληνική το σπίτι φτωχικό στις αμμουδιές του ", "ღმერთსი შემვედრე, ნუთუ კვლა დამხსნას სოფლისა შრომასა, ცეცხლს, წყალს", "⠊⠀⠉⠁⠝⠀⠑⠁⠞⠀⠛⠇⠁⠎⠎⠀⠁⠝⠙⠀⠊⠞⠀⠙⠕⠑⠎⠝⠞⠀⠓⠥⠗⠞⠀⠍⠑", "Би шил идэй чадна, надад хортой биш", "을", "나는 유리를 먹을 수 있어요. 그래도 아프지 않아요", "ฉันกินกระจกได้ แต่มันไม่ทำให้ฉันเจ็บฉันกินกระจกได้ แต่มันไม่ทำให้ฉันเจ็บ", "Ég get etið gler án þess að meiða mig.", "Mogę jeść szkło, i mi nie szkodzi.", "\ufffd\u10102\u2f877", "Начало музыкальной карьеры\nБритни пела в церковном хоре местной баптистской церкви. В возрасте 8-ми лет Спирс прошла аудирование для участия в шоу «Новый Клуб Микки-Мауса» на канале «Дисней». И хотя продюсеры решили, что Спирс слишком молода для участия в шоу, они представили её агенту в Нью-Йорке. Следующие 3 года Бритни училась в актёрской школе Professional Performing Arts School в Нью-Йорке и участвовала в нескольких постановках, в том числе «Ruthless!» 1991 года. В 1992 году Спирс участвовала в конкурсе Star Search, но проиграла во втором туре.\nВ 1993 году Спирс вернулась на канал «Дисней» и в течение 2-х лет участвовала в шоу «Новый Клуб Микки-Мауса». Другие будущие знаменитости, начинавшие с этого шоу — Кристина Агилера, участники 'N Sync Джастин Тимберлейк и Джейси Шазе, звезда сериала «Счастье» Кери Расселл и актёр фильма «Дневник памяти» Райан Гослинг.\nВ 1994 году шоу закрыли, Бритни вернулась домой в Луизиану, где поступила в среднюю школу. Некоторое время она пела в девичьей группе Innosense, но вскоре, решив начать сольную карьеру, записала демодиск, который попал в руки продюсерам из Jive Records, и те заключили с ней контракт.\nДалее последовал тур по стране, выступления в супермаркетах и работа на разогреве у групп 'N Sync и Backstreet Boys.\n[править]1999—2000: Ранний коммерческий успех\nВ октябре 1998 года вышел дебютный сингл Бритни Спирс «…Baby One More Time» . Песня имела огромный успех, в первые же недели возглавила международные чарты, мировые продажи сингла составили 9 миллионов копий, что сделало диск дважды платиновым. Альбом с одноимённым названием вышел в январе 1999 года. Альбом стартовал на первом месте рейтинга Billboard 200, пятьдесят одну неделю продержался в верхней десятке и шестьдесят недель в двадцати лучших. Альбом стал 15-кратным платиновым и на сегодняшний день является самым успешным альбомом Бритни Спирс.\nВ 1999 году Бритни снялась для апрельского номера журнала Rolling Stone. Откровенные фотографии спровоцировали слухи о том, что 17-летняя звезда сделала операцию по увеличению груди, что сама Спирс отрицала. Успех альбома и противоречивый образ Спирс, созданный массмедиа, сделали её главной звездой 1999 года.\nВслед за успешным дебютом последовал второй альбом певицы «Oops!... I Did It Again», также стартовавший на 1-м месте в США. Продажи за первую неделю составили 1 319 193 копии, что являлось абсолютным рекордом, который затем побил американский рэпер Эминем. Летом 2000 года Спирс отправилась в свой первый мировой тур, «Oops!… I Did It Again World Tour». В 2000 году Спирс получила две награды Billboards Music Awards и была номинирована на «Грэмми» в двух категориях — «Лучший поп-альбом» и «Лучшее живое выступление».\n[править]2001—2003: Вершина карьеры\n\n\nИсполняя «Me Against the Music»\nУспех Спирс сделал её заметной фигурой и в музыкальной индустрии, и в поп-культуре. В начале 2001 года она привлекла внимание «Пепси», эта компания предложила ей многомиллионный контракт, включавший телевизионную рекламу и участие в промо-акциях.\nВ ноябре 2001 года вышел третий альбом Спирс — Britney. Альбом дебютировал на первом месте в США с продажами в 745 744 пластинок за первую неделю, что сделало Бритни первой в истории исполнительницей, чьи первые три альбома стартовали на вершине рейтинга. Сразу же после выхода альбома Спирс отправилась в тур Dream Within a Dream Tour, по окончании которого объявила, что хочет взять 6-месячный перерыв в карьере.\nВ этом же году Спирс рассталась с солистом 'N Sync Джастином Тимберлейком, с которым встречалась 4 года.\nБритни вернулась на сцену в августе 2003 года.\nВ ноябре 2003 года вышел четвёртый студийный альбом Спирс In The Zone. Бритни участвовала в написании восьми из тринадцати композиций, а также выступила в качестве продюсера альбома. In The Zone дебютировал на первом месте в США, что сделало Бритни первой в истории исполнительницей, чьи первые четыре альбома стартовали на вершине рейтинга. Самый успешный сингл с альбома — Toxic — принёс Бритни первую для неё награду Грэмми в категории «Лучшая танцевальная композиция».\n[править]2007—2008: Возвращение к музыке\nВ начале 2007 года после двухлетнего перерыва Спирс приступила к записи нового сольного альбома, продюсерами которого выступили Nate «Danja» Hills, Шон Гарретт и Джонатан Ротэм.\nВ мае 2007 года Спирс в составе коллектива «The M and M’s» дала 6 концертов в рамках тура «House of Blues» в Лос-Анджелесе, Сан-Диего, Анахайме, Лас-Вегасе, Орландо и Майами. Каждый концерт длился около 15 минут и включал 5 старых хитов певицы.[4]\n30 августа 2007 года на волнах нью-йоркской радиостанции Z100 состоялась премьера песни «Gimme More», первого сингла с нового альбома Спирс.[5] Сингл вышел на iTunes 24 сентября и на CD 29 октября 2007.\n9 сентября 2007 года Спирс исполнила «Gimme More» на церемонии вручения наград MTV Video Music Awards. Выступление оказалось неудачным; Спирс выглядела непрофессионально — не всегда попадала в фонограмму и в танце отставала от группы хореографической поддержки.[6]\nНесмотря на это, в начале октября 2007 года сингл «Gimme More» достиг 3-го места в чарте Billboard Hot 100, став таким образом одним из самых успешных синглов Спирс.[7]"]; 75 | return echo_factory_factory(protocol, messages); 76 | }; 77 | 78 | factor_echo_special_chars = function(protocol) { 79 | var messages; 80 | messages = [" ", "\u0000", "\xff", "\xff\x00", "\x00\xff", " \r ", " \n ", " \r\n ", "\r\n", "", "message\t", "\tmessage", "message ", " message", "message\r", "\rmessage", "message\n", "\nmessage", "message\xff", "\xffmessage", "A", "b", "c", "d", "e", "\ufffd", "\ufffd\u0000", "message\ufffd", "\ufffdmessage"]; 81 | return echo_factory_factory(protocol, messages); 82 | }; 83 | 84 | factor_echo_large_message = function(protocol) { 85 | var messages; 86 | messages = [Array(Math.pow(2, 1)).join('x'), Array(Math.pow(2, 2)).join('x'), Array(Math.pow(2, 4)).join('x'), Array(Math.pow(2, 8)).join('x'), Array(Math.pow(2, 13)).join('x'), Array(Math.pow(2, 13)).join('x')]; 87 | return echo_factory_factory(protocol, messages); 88 | }; 89 | 90 | batch_factory_factory = function(protocol, messages) { 91 | return function() { 92 | var counter, r; 93 | expect(3 + messages.length); 94 | r = newSockJS('/echo', protocol); 95 | ok(r); 96 | counter = 0; 97 | r.onopen = function(e) { 98 | var msg, _i, _len, _results; 99 | ok(true); 100 | _results = []; 101 | for (_i = 0, _len = messages.length; _i < _len; _i++) { 102 | msg = messages[_i]; 103 | _results.push(r.send(msg)); 104 | } 105 | return _results; 106 | }; 107 | r.onmessage = function(e) { 108 | equals(e.data, messages[counter]); 109 | counter += 1; 110 | if (counter === messages.length) return r.close(); 111 | }; 112 | return r.onclose = function(e) { 113 | if (counter !== messages.length) { 114 | ok(false, "Transport closed prematurely. " + e); 115 | } else { 116 | ok(true); 117 | } 118 | return start(); 119 | }; 120 | }; 121 | }; 122 | 123 | factor_batch_large = function(protocol) { 124 | var messages; 125 | messages = [Array(Math.pow(2, 1)).join('x'), Array(Math.pow(2, 2)).join('x'), Array(Math.pow(2, 4)).join('x'), Array(Math.pow(2, 8)).join('x'), Array(Math.pow(2, 13)).join('x'), Array(Math.pow(2, 13)).join('x')]; 126 | return batch_factory_factory(protocol, messages); 127 | }; 128 | 129 | batch_factory_factory_amp = function(protocol, messages) { 130 | return function() { 131 | var counter, r; 132 | expect(3 + messages.length); 133 | r = newSockJS('/amplify', protocol); 134 | ok(r); 135 | counter = 0; 136 | r.onopen = function(e) { 137 | var msg, _i, _len, _results; 138 | ok(true); 139 | _results = []; 140 | for (_i = 0, _len = messages.length; _i < _len; _i++) { 141 | msg = messages[_i]; 142 | _results.push(r.send('' + msg)); 143 | } 144 | return _results; 145 | }; 146 | r.onmessage = function(e) { 147 | equals(e.data.length, Math.pow(2, messages[counter]), e.data); 148 | counter += 1; 149 | if (counter === messages.length) return r.close(); 150 | }; 151 | return r.onclose = function(e) { 152 | if (counter !== messages.length) { 153 | ok(false, "Transport closed prematurely. " + e); 154 | } else { 155 | ok(true); 156 | } 157 | return start(); 158 | }; 159 | }; 160 | }; 161 | 162 | factor_batch_large_amp = function(protocol) { 163 | var messages; 164 | messages = [1, 2, 4, 8, 13, 15, 15]; 165 | return batch_factory_factory_amp(protocol, messages); 166 | }; 167 | 168 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u2000-\u20ff\ufeff\ufff0-\uffff\x00-\x1f\ufffe\uffff\u0300-\u0333\u033d-\u0346\u034a-\u034c\u0350-\u0352\u0357-\u0358\u035c-\u0362\u0374\u037e\u0387\u0591-\u05af\u05c4\u0610-\u0617\u0653-\u0654\u0657-\u065b\u065d-\u065e\u06df-\u06e2\u06eb-\u06ec\u0730\u0732-\u0733\u0735-\u0736\u073a\u073d\u073f-\u0741\u0743\u0745\u0747\u07eb-\u07f1\u0951\u0958-\u095f\u09dc-\u09dd\u09df\u0a33\u0a36\u0a59-\u0a5b\u0a5e\u0b5c-\u0b5d\u0e38-\u0e39\u0f43\u0f4d\u0f52\u0f57\u0f5c\u0f69\u0f72-\u0f76\u0f78\u0f80-\u0f83\u0f93\u0f9d\u0fa2\u0fa7\u0fac\u0fb9\u1939-\u193a\u1a17\u1b6b\u1cda-\u1cdb\u1dc0-\u1dcf\u1dfc\u1dfe\u1f71\u1f73\u1f75\u1f77\u1f79\u1f7b\u1f7d\u1fbb\u1fbe\u1fc9\u1fcb\u1fd3\u1fdb\u1fe3\u1feb\u1fee-\u1fef\u1ff9\u1ffb\u1ffd\u2000-\u2001\u20d0-\u20d1\u20d4-\u20d7\u20e7-\u20e9\u2126\u212a-\u212b\u2329-\u232a\u2adc\u302b-\u302c\uaab2-\uaab3\uf900-\ufa0d\ufa10\ufa12\ufa15-\ufa1e\ufa20\ufa22\ufa25-\ufa26\ufa2a-\ufa2d\ufa30-\ufa6d\ufa70-\ufad9\ufb1d\ufb1f\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4e]/g; 169 | 170 | generate_killer_string = function(escapable) { 171 | var c, i, s; 172 | s = []; 173 | c = (function() { 174 | var _results; 175 | _results = []; 176 | for (i = 0; i <= 65535; i++) { 177 | _results.push(String.fromCharCode(i)); 178 | } 179 | return _results; 180 | })(); 181 | escapable.lastIndex = 0; 182 | c.join('').replace(escapable, function(a) { 183 | s.push(a); 184 | return ''; 185 | }); 186 | return s.join(''); 187 | }; 188 | 189 | factor_echo_utf_encoding_simple = function(protocol) { 190 | var i, message; 191 | message = (function() { 192 | var _results; 193 | _results = []; 194 | for (i = 0; i <= 256; i++) { 195 | _results.push(String.fromCharCode(i)); 196 | } 197 | return _results; 198 | })(); 199 | return echo_factory_factory(protocol, [message.join('')]); 200 | }; 201 | 202 | factor_echo_utf_encoding = function(protocol) { 203 | var message; 204 | message = generate_killer_string(escapable); 205 | return echo_factory_factory(protocol, [message]); 206 | }; 207 | 208 | factor_user_close = function(protocol) { 209 | return function() { 210 | var counter, r; 211 | expect(5); 212 | r = newSockJS('/echo', protocol); 213 | ok(r); 214 | counter = 0; 215 | r.onopen = function(e) { 216 | counter += 1; 217 | ok(counter === 1); 218 | r.close(3000, "User message"); 219 | return ok(counter === 1); 220 | }; 221 | r.onmessage = function() { 222 | ok(false); 223 | return counter += 1; 224 | }; 225 | return r.onclose = function(e) { 226 | counter += 1; 227 | log('user_close ' + e.code + ' ' + e.reason); 228 | equals(e.wasClean, true); 229 | ok(counter === 2); 230 | return start(); 231 | }; 232 | }; 233 | }; 234 | 235 | factor_server_close = function(protocol) { 236 | return function() { 237 | var r; 238 | expect(5); 239 | r = newSockJS('/close', protocol); 240 | ok(r); 241 | r.onopen = function(e) { 242 | return ok(true); 243 | }; 244 | r.onmessage = function(e) { 245 | return ok(false); 246 | }; 247 | return r.onclose = function(e) { 248 | equals(e.code, 3000); 249 | equals(e.reason, "Go away!"); 250 | equals(e.wasClean, true); 251 | return start(); 252 | }; 253 | }; 254 | }; 255 | 256 | arrIndexOf = function(arr, obj) { 257 | var i, _ref; 258 | for (i = 0, _ref = arr.length; 0 <= _ref ? i < _ref : i > _ref; 0 <= _ref ? i++ : i--) { 259 | if (arr[i] === obj) return i; 260 | } 261 | return -1; 262 | }; 263 | 264 | test_protocol_messages = function(protocol) { 265 | module(protocol); 266 | if (!SockJS[protocol] || !SockJS[protocol].enabled()) { 267 | return test("[unsupported by client]", function() { 268 | return log('Unsupported protocol (by client): "' + protocol + '"'); 269 | }); 270 | } else if (client_opts.disabled_transports && arrIndexOf(client_opts.disabled_transports, protocol) !== -1) { 271 | return test("[disabled by config]", function() { 272 | return log('Disabled by config: "' + protocol + '"'); 273 | }); 274 | } else { 275 | asyncTest("echo1", factor_echo_basic(protocol)); 276 | asyncTest("echo2", factor_echo_rich(protocol)); 277 | asyncTest("unicode", factor_echo_unicode(protocol)); 278 | asyncTest("utf encoding 0x00-0xFF", factor_echo_utf_encoding_simple(protocol)); 279 | asyncTest("utf encoding killer message", factor_echo_utf_encoding(protocol)); 280 | asyncTest("special_chars", factor_echo_special_chars(protocol)); 281 | asyncTest("large message (ping-pong)", factor_echo_large_message(protocol)); 282 | asyncTest("large message (batch)", factor_batch_large(protocol)); 283 | asyncTest("large download", factor_batch_large_amp(protocol)); 284 | asyncTest("user close", factor_user_close(protocol)); 285 | return asyncTest("server close", factor_server_close(protocol)); 286 | } 287 | }; 288 | 289 | for (_i = 0, _len = protocols.length; _i < _len; _i++) { 290 | protocol = protocols[_i]; 291 | test_protocol_messages(protocol); 292 | } 293 | -------------------------------------------------------------------------------- /qunit/html/static/qunit.min.js: -------------------------------------------------------------------------------- 1 | (function(a){function t(a){var b="",c;for(var d=0;a[d];d++)c=a[d],c.nodeType===3||c.nodeType===4?b+=c.nodeValue:c.nodeType!==8&&(b+=t(c.childNodes));return b}function s(a){return typeof document!="undefined"&&!!document&&!!document.getElementById&&document.getElementById(a)}function r(a,b,c){a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent?a.attachEvent("on"+b,c):c()}function q(a,b){for(var c in b)b[c]===undefined?delete a[c]:a[c]=b[c];return a}function p(b,c,d){typeof console!="undefined"&&console.error&&console.warn?(console.error(b),console.error(c),console.warn(d.toString())):a.opera&&opera.postError&&opera.postError(b,c,d.toString)}function o(a,b){var c=a.slice();for(var d=0;d0&&ok(!1,"Introduced global variable(s): "+c.join(", "));var d=o(b,f.pollution);d.length>0&&ok(!1,"Deleted global variable(s): "+d.join(", "))}function m(){f.pollution=[];if(f.noglobals)for(var b in a)f.pollution.push(b)}function l(){var b=(new Date).getTime();while(f.queue.length&&!f.blocking)if(f.updateRate<=0||(new Date).getTime()-b\\]/g,function(a){switch(a){case"&":return"&";case"\\":return"\\\\";case'"':return'"';case"<":return"<";case">":return">";default:return a}})}function i(){try{throw new Error}catch(a){if(a.stacktrace)return a.stacktrace.split("\n")[6];if(a.stack)return a.stack.split("\n")[4]}}function h(a){var b=f.filter,c=!1;if(!b)return!0;var d=b.charAt(0)==="!";d&&(b=b.slice(1));if(a.indexOf(b)!==-1)return!d;d&&(c=!0);return c}function g(){f.autorun=!0,f.currentModule&&e.moduleDone({name:f.currentModule,failed:f.moduleStats.bad,passed:f.moduleStats.all-f.moduleStats.bad,total:f.moduleStats.all});var a=s("qunit-banner"),b=s("qunit-tests"),c=+(new Date)-f.started,d=f.stats.all-f.stats.bad,g=["Tests completed in ",c," milliseconds.
      ",'',d,' tests of ',f.stats.all,' passed, ',f.stats.bad," failed."].join("");a&&(a.className=f.stats.bad?"qunit-fail":"qunit-pass"),b&&(s("qunit-testresult").innerHTML=g),typeof document!="undefined"&&document.title&&(document.title=(f.stats.bad?"✖":"✔")+" "+document.title),e.done({failed:f.stats.bad,passed:d,total:f.stats.all,runtime:c})}var b={setTimeout:typeof a.setTimeout!="undefined",sessionStorage:function(){try{return!!sessionStorage.getItem}catch(a){return!1}}()},c=0,d=function(a,b,c,d,e,f){this.name=a,this.testName=b,this.expected=c,this.testEnvironmentArg=d,this.async=e,this.callback=f,this.assertions=[]};d.prototype={init:function(){var a=s("qunit-tests");if(a){var b=document.createElement("strong");b.innerHTML="Running "+this.name;var d=document.createElement("li");d.appendChild(b),d.className="running",d.id=this.id="test-output"+c++,a.appendChild(d)}},setup:function(){this.module!=f.previousModule&&(f.previousModule&&e.moduleDone({name:f.previousModule,failed:f.moduleStats.bad,passed:f.moduleStats.all-f.moduleStats.bad,total:f.moduleStats.all}),f.previousModule=this.module,f.moduleStats={all:0,bad:0},e.moduleStart({name:this.module})),f.current=this,this.testEnvironment=q({setup:function(){},teardown:function(){}},this.moduleTestEnvironment),this.testEnvironmentArg&&q(this.testEnvironment,this.testEnvironmentArg),e.testStart({name:this.testName}),e.current_testEnvironment=this.testEnvironment;try{f.pollution||m(),this.testEnvironment.setup.call(this.testEnvironment)}catch(a){e.ok(!1,"Setup failed on "+this.testName+": "+a.message)}},run:function(){this.async&&e.stop();if(f.notrycatch)this.callback.call(this.testEnvironment);else try{this.callback.call(this.testEnvironment)}catch(a){p("Test "+this.testName+" died, exception and test follows",a,this.callback),e.ok(!1,"Died on test #"+(this.assertions.length+1)+": "+a.message+" - "+e.jsDump.parse(a)),m(),f.blocking&&start()}},teardown:function(){try{this.testEnvironment.teardown.call(this.testEnvironment),n()}catch(a){e.ok(!1,"Teardown failed on "+this.testName+": "+a.message)}},finish:function(){this.expected&&this.expected!=this.assertions.length&&e.ok(!1,"Expected "+this.expected+" assertions, but "+this.assertions.length+" were run");var c=0,d=0,g=s("qunit-tests");f.stats.all+=this.assertions.length,f.moduleStats.all+=this.assertions.length;if(g){var h=document.createElement("ol");for(var i=0;i("+d+", "+c+", "+this.assertions.length+")";var m=document.createElement("a");m.innerHTML="Rerun",m.href=e.url({filter:t([l]).replace(/\([^)]+\)$/,"").replace(/(^\s*|\s*$)/g,"")}),r(l,"click",function(){var a=l.nextSibling.nextSibling,b=a.style.display;a.style.display=b==="none"?"block":"none"}),r(l,"dblclick",function(b){var c=b&&b.target?b.target:a.event.srcElement;if(c.nodeName.toLowerCase()=="span"||c.nodeName.toLowerCase()=="b")c=c.parentNode;a.location&&c.nodeName.toLowerCase()==="strong"&&(a.location=e.url({filter:t([c]).replace(/\([^)]+\)$/,"").replace(/(^\s*|\s*$)/g,"")}))});var k=s(this.id);k.className=d?"fail":"pass",k.removeChild(k.firstChild),k.appendChild(l),k.appendChild(m),k.appendChild(h)}else for(var i=0;i",i;arguments.length===2&&(c=b,b=null),b&&typeof b=="object"&&(i=b,b=null),f.currentModule&&(g=''+f.currentModule+": "+g);if(!!h(f.currentModule+": "+a)){var j=new d(g,a,b,i,e,c);j.module=f.currentModule,j.moduleTestEnvironment=f.currentModuleTestEnviroment,j.queue()}},expect:function(a){f.current.expected=a},ok:function(a,b){a=!!a;var c={result:a,message:b};b=j(b),e.log(c),f.current.assertions.push({result:a,message:b})},equal:function(a,b,c){e.push(b==a,a,b,c)},notEqual:function(a,b,c){e.push(b!=a,a,b,c)},deepEqual:function(a,b,c){e.push(e.equiv(a,b),a,b,c)},notDeepEqual:function(a,b,c){e.push(!e.equiv(a,b),a,b,c)},strictEqual:function(a,b,c){e.push(b===a,a,b,c)},notStrictEqual:function(a,b,c){e.push(b!==a,a,b,c)},raises:function(a,b,c){var d,f=!1;typeof b=="string"&&(c=b,b=null);try{a()}catch(g){d=g}d&&(b?e.objectType(b)==="regexp"?f=b.test(d):d instanceof b?f=!0:b.call({},d)===!0&&(f=!0):f=!0),e.ok(f,c)},start:function(){f.semaphore--;f.semaphore>0||(f.semaphore<0&&(f.semaphore=0),b.setTimeout?a.setTimeout(function(){f.timeout&&clearTimeout(f.timeout),f.blocking=!1,l()},13):(f.blocking=!1,l()))},stop:function(c){f.semaphore++,f.blocking=!0,c&&b.setTimeout&&(clearTimeout(f.timeout),f.timeout=a.setTimeout(function(){e.ok(!1,"Test timed out"),e.start()},c))}};e.equals=e.equal,e.same=e.deepEqual;var f={queue:[],blocking:!0,reorder:!0,noglobals:!1,notrycatch:!1};(function(){var b=a.location||{search:"",protocol:"file:"},c=b.search.slice(1).split("&"),d=c.length,g={},h;if(c[0])for(var i=0;i ")},reset:function(){if(a.jQuery)jQuery("#qunit-fixture").html(f.fixture);else{var b=s("qunit-fixture");b&&(b.innerHTML=f.fixture)}},triggerEvent:function(a,b,c){document.createEvent?(c=document.createEvent("MouseEvents"),c.initMouseEvent(b,!0,!0,a.ownerDocument.defaultView,0,0,0,0,0,!1,!1,!1,!1,0,null),a.dispatchEvent(c)):a.fireEvent&&a.fireEvent("on"+b)},is:function(a,b){return e.objectType(b)==a},objectType:function(a){if(typeof a=="undefined")return"undefined";if(a===null)return"null";var b=Object.prototype.toString.call(a).match(/^\[object\s(.*)\]$/)[1]||"";switch(b){case"Number":return isNaN(a)?"nan":"number";case"String":case"Boolean":case"Array":case"Date":case"RegExp":case"Function":return b.toLowerCase()}return typeof a=="object"?"object":undefined},push:function(a,b,c,d){var g={result:a,message:d,actual:b,expected:c};d=j(d)||(a?"okay":"failed"),d=''+d+"",c=j(e.jsDump.parse(c)),b=j(e.jsDump.parse(b));var h=d+'";b!=c&&(h+='",h+='");if(!a){var k=i();k&&(g.source=k,h+='")}h+="
      Expected:
      '+c+"
      Result:
      '+b+"
      Diff:
      '+e.diff(c,b)+"
      Source:
      '+j(k)+"
      ",e.log(g),f.current.assertions.push({result:!!a,message:h})},url:function(b){b=q(q({},e.urlParams),b);var c="?",d;for(d in b)c+=encodeURIComponent(d)+"="+encodeURIComponent(b[d])+"&";return a.location.pathname+c.slice(0,-1)},begin:function(){},done:function(){},log:function(){},testStart:function(){},testDone:function(){},moduleStart:function(){},moduleDone:function(){}});if(typeof document=="undefined"||document.readyState==="complete")f.autorun=!0;r(a,"load",function(){e.begin({});var c=q({},f);e.init(),q(f,c),f.blocking=!1;var d=s("qunit-userAgent");d&&(d.innerHTML=navigator.userAgent);var g=s("qunit-header");g&&(g.innerHTML=' '+g.innerHTML+" "+'"+'",r(g,"change",function(b){var c={};c[b.target.name]=b.target.checked?!0:undefined,a.location=e.url(c)}));var h=s("qunit-testrunner-toolbar");if(h){var i=document.createElement("input");i.type="checkbox",i.id="qunit-filter-pass",r(i,"click",function(){var a=document.getElementById("qunit-tests");if(i.checked)a.className=a.className+" hidepass";else{var c=" "+a.className.replace(/[\n\t\r]/g," ")+" ";a.className=c.replace(/ hidepass /," ")}b.sessionStorage&&(i.checked?sessionStorage.setItem("qunit-filter-passed-tests","true"):sessionStorage.removeItem("qunit-filter-passed-tests"))});if(b.sessionStorage&&sessionStorage.getItem("qunit-filter-passed-tests")){i.checked=!0;var j=document.getElementById("qunit-tests");j.className=j.className+" hidepass"}h.appendChild(i);var k=document.createElement("label");k.setAttribute("for","qunit-filter-pass"),k.innerHTML="Hide passed tests",h.appendChild(k)}var l=s("qunit-fixture");l&&(f.fixture=l.innerHTML),f.autostart&&e.start()}),e.equiv=function(){function d(a,b,c){var d=e.objectType(a);if(d)return e.objectType(b[d])==="function"?b[d].apply(b,c):b[d]}var a,b=[],c=[],f=function(){function d(a,b){return a instanceof b.constructor||b instanceof a.constructor?b==a:b===a}return{string:d,"boolean":d,number:d,"null":d,"undefined":d,nan:function(a){return isNaN(a)},date:function(a,b){return e.objectType(a)==="date"&&b.valueOf()===a.valueOf()},regexp:function(a,b){return e.objectType(a)==="regexp"&&b.source===a.source&&b.global===a.global&&b.ignoreCase===a.ignoreCase&&b.multiline===a.multiline},"function":function(){var a=b[b.length-1];return a!==Object&&typeof a!="undefined"},array:function(b,d){var f,g,h,i;if(e.objectType(b)!=="array")return!1;i=d.length;if(i!==b.length)return!1;c.push(d);for(f=0;f=0?b="array":b=typeof a;return b},separator:function(){return this.multiline?this.HTML?"
      ":"\n":this.HTML?" ":" "},indent:function(a){if(!this.multiline)return"";var b=this.indentChar;this.HTML&&(b=b.replace(/\t/g," ").replace(/ /g," "));return Array(this._depth_+(a||0)).join(b)},up:function(a){this._depth_+=a||1},down:function(a){this._depth_-=a||1},setParser:function(a,b){this.parsers[a]=b},quote:a,literal:b,join:c,_depth_:1,parsers:{window:"[Window]",document:"[Document]",error:"[ERROR]",unknown:"[Unknown]","null":"null","undefined":"undefined","function":function(a){var b="function",d="name"in a?a.name:(f.exec(a)||[])[1];d&&(b+=" "+d),b+="(",b=[b,e.jsDump.parse(a,"functionArgs"),"){"].join("");return c(b,e.jsDump.parse(a,"functionCode"),"}")},array:d,nodelist:d,arguments:d,object:function(a){var b=[];e.jsDump.up();for(var d in a)b.push(e.jsDump.parse(d,"key")+": "+e.jsDump.parse(a[d]));e.jsDump.down();return c("{",b,"}")},node:function(a){var b=e.jsDump.HTML?"<":"<",c=e.jsDump.HTML?">":">",d=a.nodeName.toLowerCase(),f=b+d;for(var g in e.jsDump.DOMAttrs){var h=a[e.jsDump.DOMAttrs[g]];h&&(f+=" "+g+"="+e.jsDump.parse(h,"attribute"))}return f+c+b+"/"+d+c},functionArgs:function(a){var b=a.length;if(!b)return"";var c=Array(b);while(b--)c[b]=String.fromCharCode(97+b);return" "+c.join(", ")+" "},key:a,functionCode:"[code]",attribute:a,string:a,date:a,regexp:b,number:b,"boolean":b},DOMAttrs:{id:"id",name:"name","class":"className"},HTML:!1,indentChar:" ",multiline:!0};return g}(),e.diff=function(){function a(a,b){var c=new Object,d=new Object;for(var e=0;e0;e--)b[e].text!=null&&b[e-1].text==null&&b[e].row>0&&a[b[e].row-1].text==null&&b[e-1]==a[b[e].row-1]&&(b[e-1]={text:b[e-1],row:b[e].row-1},a[b[e].row-1]={text:a[b[e].row-1],row:e-1});return{o:a,n:b}}return function(b,c){b=b.replace(/\s+$/,""),c=c.replace(/\s+$/,"");var d=a(b==""?[]:b.split(/\s+/),c==""?[]:c.split(/\s+/)),e="",f=b.match(/\s+/g);f==null?f=[" "]:f.push(" ");var g=c.match(/\s+/g);g==null?g=[" "]:g.push(" ");if(d.n.length==0)for(var h=0;h"+d.o[h]+f[h]+"";else{if(d.n[0].text==null)for(c=0;c"+d.o[c]+f[c]+"";for(var h=0;h"+d.n[h]+g[h]+"";else{var i="";for(c=d.n[h].row+1;c"+d.o[c]+f[c]+"";e+=" "+d.n[h].text+g[h]+i}}return e}}()})(this) -------------------------------------------------------------------------------- /txsockjs/websockets.py: -------------------------------------------------------------------------------- 1 | # ===================================================================================== 2 | # === THIS IS A DIRECT COPY OF twisted.web.websockets AS IT IS STILL IN DEVELOPMENT === 3 | # === IT WILL BE REPLACED BY THE ACTUAL VERSION WHEN IT IS PUBLICLY AVAILABLE. === 4 | # ===================================================================================== 5 | # -*- test-case-name: twisted.web.test.test_websockets -*- 6 | # Copyright (c) Twisted Matrix Laboratories. 7 | # 2011-2012 Oregon State University Open Source Lab 8 | # 2011-2012 Corbin Simpson 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documentation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furnished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in 18 | # all copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | # THE SOFTWARE. 27 | 28 | """ 29 | The WebSockets protocol (RFC 6455), provided as a resource which wraps a 30 | factory. 31 | """ 32 | 33 | __all__ = ["WebSocketsResource"] 34 | 35 | from hashlib import sha1 36 | from struct import pack, unpack 37 | 38 | from zope.interface import implementer, Interface 39 | 40 | from twisted.protocols.policies import ProtocolWrapper, WrappingFactory 41 | from twisted.python import log 42 | from twisted.python.constants import NamedConstant, Names 43 | from twisted.web.resource import IResource 44 | from twisted.web.server import NOT_DONE_YET 45 | 46 | 47 | 48 | class _WSException(Exception): 49 | """ 50 | Internal exception for control flow inside the WebSockets frame parser. 51 | """ 52 | 53 | 54 | 55 | # Control frame specifiers. Some versions of WS have control signals sent 56 | # in-band. Adorable, right? 57 | 58 | class _CONTROLS(Names): 59 | """ 60 | Control frame specifiers. 61 | """ 62 | 63 | NORMAL = NamedConstant() 64 | CLOSE = NamedConstant() 65 | PING = NamedConstant() 66 | PONG = NamedConstant() 67 | 68 | 69 | _opcodeTypes = { 70 | 0x0: _CONTROLS.NORMAL, 71 | 0x1: _CONTROLS.NORMAL, 72 | 0x2: _CONTROLS.NORMAL, 73 | 0x8: _CONTROLS.CLOSE, 74 | 0x9: _CONTROLS.PING, 75 | 0xa: _CONTROLS.PONG} 76 | 77 | 78 | _opcodeForType = { 79 | _CONTROLS.NORMAL: 0x1, 80 | _CONTROLS.CLOSE: 0x8, 81 | _CONTROLS.PING: 0x9, 82 | _CONTROLS.PONG: 0xa} 83 | 84 | 85 | # Authentication for WS. 86 | 87 | # The GUID for WebSockets, from RFC 6455. 88 | _WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 89 | 90 | 91 | 92 | def _makeAccept(key): 93 | """ 94 | Create an "accept" response for a given key. 95 | 96 | This dance is expected to somehow magically make WebSockets secure. 97 | 98 | @type key: C{str} 99 | @param key: The key to respond to. 100 | 101 | @rtype: C{str} 102 | @return: An encoded response. 103 | """ 104 | return sha1("%s%s" % (key, _WS_GUID)).digest().encode("base64").strip() 105 | 106 | 107 | 108 | # Frame helpers. 109 | # Separated out to make unit testing a lot easier. 110 | # Frames are bonghits in newer WS versions, so helpers are appreciated. 111 | 112 | 113 | 114 | def _mask(buf, key): 115 | """ 116 | Mask or unmask a buffer of bytes with a masking key. 117 | 118 | @type buf: C{str} 119 | @param buf: A buffer of bytes. 120 | 121 | @type key: C{str} 122 | @param key: The masking key. Must be exactly four bytes. 123 | 124 | @rtype: C{str} 125 | @return: A masked buffer of bytes. 126 | """ 127 | 128 | # This is super-secure, I promise~ 129 | key = [ord(i) for i in key] 130 | buf = list(buf) 131 | for i, char in enumerate(buf): 132 | buf[i] = chr(ord(char) ^ key[i % 4]) 133 | return "".join(buf) 134 | 135 | 136 | 137 | def _makeFrame(buf, _opcode=_CONTROLS.NORMAL): 138 | """ 139 | Make a frame. 140 | 141 | This function always creates unmasked frames, and attempts to use the 142 | smallest possible lengths. 143 | 144 | @type buf: C{str} 145 | @param buf: A buffer of bytes. 146 | 147 | @type _opcode: C{_CONTROLS} 148 | @param _opcode: Which type of frame to create. 149 | 150 | @rtype: C{str} 151 | @return: A packed frame. 152 | """ 153 | bufferLength = len(buf) 154 | 155 | if bufferLength > 0xffff: 156 | length = "\x7f%s" % pack(">Q", bufferLength) 157 | elif bufferLength > 0x7d: 158 | length = "\x7e%s" % pack(">H", bufferLength) 159 | else: 160 | length = chr(bufferLength) 161 | 162 | # Always make a normal packet. 163 | header = chr(0x80 | _opcodeForType[_opcode]) 164 | frame = "%s%s%s" % (header, length, buf) 165 | return frame 166 | 167 | 168 | 169 | def _parseFrames(buf): 170 | """ 171 | Parse frames in a highly compliant manner. 172 | 173 | @type buf: C{str} 174 | @param buf: A buffer of bytes. 175 | 176 | @rtype: C{list} 177 | @return: A list of frames. 178 | """ 179 | start = 0 180 | frames = [] 181 | 182 | while True: 183 | # If there's not at least two bytes in the buffer, bail. 184 | if len(buf) - start < 2: 185 | break 186 | 187 | # Grab the header. This single byte holds some flags nobody cares 188 | # about, and an opcode which nobody cares about. 189 | header = ord(buf[start]) 190 | if header & 0x70: 191 | # At least one of the reserved flags is set. Pork chop sandwiches! 192 | raise _WSException("Reserved flag in frame (%d)" % header) 193 | 194 | # Get the opcode, and translate it to a local enum which we actually 195 | # care about. 196 | opcode = header & 0xf 197 | try: 198 | opcode = _opcodeTypes[opcode] 199 | except KeyError: 200 | raise _WSException("Unknown opcode %d in frame" % opcode) 201 | 202 | # Get the payload length and determine whether we need to look for an 203 | # extra length. 204 | length = ord(buf[start + 1]) 205 | masked = length & 0x80 206 | length &= 0x7f 207 | 208 | # The offset we're gonna be using to walk through the frame. We use 209 | # this because the offset is variable depending on the length and 210 | # mask. 211 | offset = 2 212 | 213 | # Extra length fields. 214 | if length == 0x7e: 215 | if len(buf) - start < 4: 216 | break 217 | 218 | length = buf[start + 2:start + 4] 219 | length = unpack(">H", length)[0] 220 | offset += 2 221 | elif length == 0x7f: 222 | if len(buf) - start < 10: 223 | break 224 | 225 | # Protocol bug: The top bit of this long long *must* be cleared; 226 | # that is, it is expected to be interpreted as signed. That's 227 | # fucking stupid, if you don't mind me saying so, and so we're 228 | # interpreting it as unsigned anyway. If you wanna send exabytes 229 | # of data down the wire, then go ahead! 230 | length = buf[start + 2:start + 10] 231 | length = unpack(">Q", length)[0] 232 | offset += 8 233 | 234 | if masked: 235 | if len(buf) - (start + offset) < 4: 236 | # This is not strictly necessary, but it's more explicit so 237 | # that we don't create an invalid key. 238 | break 239 | 240 | key = buf[start + offset:start + offset + 4] 241 | offset += 4 242 | 243 | if len(buf) - (start + offset) < length: 244 | break 245 | 246 | data = buf[start + offset:start + offset + length] 247 | 248 | if masked: 249 | data = _mask(data, key) 250 | 251 | if opcode == _CONTROLS.CLOSE: 252 | if len(data) >= 2: 253 | # Gotta unpack the opcode and return usable data here. 254 | data = unpack(">H", data[:2])[0], data[2:] 255 | else: 256 | # No reason given; use generic data. 257 | data = 1000, "No reason given" 258 | 259 | frames.append((opcode, data)) 260 | start += offset + length 261 | 262 | return frames, buf[start:] 263 | 264 | 265 | 266 | class _WebSocketsProtocol(ProtocolWrapper): 267 | """ 268 | Protocol which wraps another protocol to provide a WebSockets transport 269 | layer. 270 | """ 271 | _buffer = None 272 | 273 | 274 | def connectionMade(self): 275 | """ 276 | Log the new connection and initialize the buffer list. 277 | """ 278 | ProtocolWrapper.connectionMade(self) 279 | log.msg("Opening connection with %s" % self.transport.getPeer()) 280 | self._buffer = [] 281 | 282 | 283 | def _parseFrames(self): 284 | """ 285 | Find frames in incoming data and pass them to the underlying protocol. 286 | """ 287 | try: 288 | frames, rest = _parseFrames("".join(self._buffer)) 289 | except _WSException: 290 | # Couldn't parse all the frames, something went wrong, let's bail. 291 | log.err() 292 | self.loseConnection() 293 | return 294 | 295 | self._buffer[:] = [rest] 296 | 297 | for frame in frames: 298 | opcode, data = frame 299 | if opcode == _CONTROLS.NORMAL: 300 | # Business as usual. Decode the frame, if we have a decoder. 301 | # Pass the frame to the underlying protocol. 302 | ProtocolWrapper.dataReceived(self, data) 303 | elif opcode == _CONTROLS.CLOSE: 304 | # The other side wants us to close. I wonder why? 305 | reason, text = data 306 | log.msg("Closing connection: %r (%d)" % (text, reason)) 307 | 308 | # Close the connection. 309 | self.loseConnection() 310 | return 311 | elif opcode == _CONTROLS.PING: 312 | # 5.5.2 PINGs must be responded to with PONGs. 313 | # 5.5.3 PONGs must contain the data that was sent with the 314 | # provoking PING. 315 | self.transport.write(_makeFrame(data, _opcode=_CONTROLS.PONG)) 316 | 317 | 318 | def _sendFrames(self, frames): 319 | """ 320 | Send all pending frames. 321 | 322 | @param frames: A list of byte strings to send. 323 | @type frames: C{list} 324 | """ 325 | for frame in frames: 326 | # Encode the frame before sending it. 327 | packet = _makeFrame(frame) 328 | self.transport.write(packet) 329 | 330 | 331 | def dataReceived(self, data): 332 | """ 333 | Append the data to the buffer list and parse the whole. 334 | """ 335 | self._buffer.append(data) 336 | 337 | self._parseFrames() 338 | 339 | 340 | def write(self, data): 341 | """ 342 | Write to the transport. 343 | 344 | This method will only be called by the underlying protocol. 345 | """ 346 | self._sendFrames([data]) 347 | 348 | 349 | def writeSequence(self, data): 350 | """ 351 | Write a sequence of data to the transport. 352 | 353 | This method will only be called by the underlying protocol. 354 | """ 355 | self._sendFrames(data) 356 | 357 | 358 | def loseConnection(self): 359 | """ 360 | Close the connection. 361 | 362 | This includes telling the other side we're closing the connection. 363 | 364 | If the other side didn't signal that the connection is being closed, 365 | then we might not see their last message, but since their last message 366 | should, according to the spec, be a simple acknowledgement, it 367 | shouldn't be a problem. 368 | """ 369 | # Send a closing frame. It's only polite. (And might keep the browser 370 | # from hanging.) 371 | if not self.disconnecting: 372 | frame = _makeFrame("", _opcode=_CONTROLS.CLOSE) 373 | self.transport.write(frame) 374 | 375 | ProtocolWrapper.loseConnection(self) 376 | 377 | 378 | 379 | class _WebSocketsFactory(WrappingFactory): 380 | """ 381 | Factory which wraps another factory to provide WebSockets frames for all 382 | of its protocols. 383 | 384 | This factory does not provide the HTTP headers required to perform a 385 | WebSockets handshake; see C{WebSocketsResource}. 386 | """ 387 | protocol = _WebSocketsProtocol 388 | 389 | 390 | 391 | class IWebSocketsResource(Interface): 392 | """ 393 | A WebSockets resource. 394 | 395 | @since: 13.0 396 | """ 397 | 398 | def lookupProtocol(protocolNames, request): 399 | """ 400 | Build a protocol instance for the given protocol options and request. 401 | 402 | @param protocolNames: The asked protocols from the client. 403 | @type protocolNames: C{list} of C{str} 404 | 405 | @param request: The connecting client request. 406 | @type request: L{IRequest} 407 | 408 | @return: A tuple of (protocol, C{None}). 409 | @rtype: C{tuple} 410 | """ 411 | 412 | 413 | 414 | @implementer(IResource, IWebSocketsResource) 415 | class WebSocketsResource(object): 416 | """ 417 | A resource for serving a protocol through WebSockets. 418 | 419 | This class wraps a factory and connects it to WebSockets clients. Each 420 | connecting client will be connected to a new protocol of the factory. 421 | 422 | Due to unresolved questions of logistics, this resource cannot have 423 | children. 424 | 425 | @since: 13.0 426 | """ 427 | isLeaf = True 428 | 429 | def __init__(self, factory): 430 | self._factory = _WebSocketsFactory(factory) 431 | 432 | 433 | def getChildWithDefault(self, name, request): 434 | """ 435 | Reject attempts to retrieve a child resource. All path segments beyond 436 | the one which refers to this resource are handled by the WebSocket 437 | connection. 438 | """ 439 | raise RuntimeError( 440 | "Cannot get IResource children from WebsocketsResourceTest") 441 | 442 | 443 | def putChild(self, path, child): 444 | """ 445 | Reject attempts to add a child resource to this resource. The 446 | WebSocket connection handles all path segments beneath this resource, 447 | so L{IResource} children can never be found. 448 | """ 449 | raise RuntimeError( 450 | "Cannot put IResource children under WebSocketsResource") 451 | 452 | 453 | def lookupProtocol(self, protocolNames, request): 454 | """ 455 | Build a protocol instance for the given protocol options and request. 456 | This default implementation ignores the protocols and just return an 457 | instance of protocols built by C{self._factory}. 458 | 459 | @param protocolNames: The asked protocols from the client. 460 | @type protocolNames: C{list} of C{str} 461 | 462 | @param request: The connecting client request. 463 | @type request: L{Request} 464 | 465 | @return: A tuple of (protocol, C{None}). 466 | @rtype: C{tuple} 467 | """ 468 | protocol = self._factory.buildProtocol(request.transport.getPeer()) 469 | return protocol, None 470 | 471 | 472 | def render(self, request): 473 | """ 474 | Render a request. 475 | 476 | We're not actually rendering a request. We are secretly going to handle 477 | a WebSockets connection instead. 478 | 479 | @param request: The connecting client request. 480 | @type request: L{Request} 481 | 482 | @return: a strinf if the request fails, otherwise C{NOT_DONE_YET}. 483 | """ 484 | request.defaultContentType = None 485 | # If we fail at all, we're gonna fail with 400 and no response. 486 | # You might want to pop open the RFC and read along. 487 | failed = False 488 | 489 | if request.method != "GET": 490 | # 4.2.1.1 GET is required. 491 | failed = True 492 | 493 | upgrade = request.getHeader("Upgrade") 494 | if upgrade is None or "websocket" not in upgrade.lower(): 495 | # 4.2.1.3 Upgrade: WebSocket is required. 496 | failed = True 497 | 498 | connection = request.getHeader("Connection") 499 | if connection is None or "upgrade" not in connection.lower(): 500 | # 4.2.1.4 Connection: Upgrade is required. 501 | failed = True 502 | 503 | key = request.getHeader("Sec-WebSocket-Key") 504 | if key is None: 505 | # 4.2.1.5 The challenge key is required. 506 | failed = True 507 | 508 | version = request.getHeader("Sec-WebSocket-Version") 509 | if version != "13": 510 | # 4.2.1.6 Only version 13 works. 511 | failed = True 512 | # 4.4 Forward-compatible version checking. 513 | request.setHeader("Sec-WebSocket-Version", "13") 514 | 515 | if failed: 516 | request.setResponseCode(400) 517 | return "" 518 | 519 | askedProtocols = request.requestHeaders.getRawHeaders( 520 | "Sec-WebSocket-Protocol") 521 | protocol, protocolName = self.lookupProtocol(askedProtocols, request) 522 | 523 | # If a protocol is not created, we deliver an error status. 524 | if not protocol.wrappedProtocol: 525 | request.setResponseCode(502) 526 | return "" 527 | 528 | # We are going to finish this handshake. We will return a valid status 529 | # code. 530 | # 4.2.2.5.1 101 Switching Protocols 531 | request.setResponseCode(101) 532 | # 4.2.2.5.2 Upgrade: websocket 533 | request.setHeader("Upgrade", "WebSocket") 534 | # 4.2.2.5.3 Connection: Upgrade 535 | request.setHeader("Connection", "Upgrade") 536 | # 4.2.2.5.4 Response to the key challenge 537 | request.setHeader("Sec-WebSocket-Accept", _makeAccept(key)) 538 | # 4.2.2.5.5 Optional codec declaration 539 | if protocolName: 540 | request.setHeader("Sec-WebSocket-Protocol", protocolName) 541 | 542 | # Provoke request into flushing headers and finishing the handshake. 543 | request.write("") 544 | 545 | # And now take matters into our own hands. We shall manage the 546 | # transport's lifecycle. 547 | transport, request.transport = request.transport, None 548 | 549 | # Connect the transport to our factory, and make things go. We need to 550 | # do some stupid stuff here; see #3204, which could fix it. 551 | if request.isSecure(): 552 | # Secure connections wrap in TLSMemoryBIOProtocol too. 553 | transport.protocol.wrappedProtocol = protocol 554 | else: 555 | transport.protocol = protocol 556 | protocol.makeConnection(transport) 557 | 558 | return NOT_DONE_YET 559 | -------------------------------------------------------------------------------- /txsockjs/oldwebsockets.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012, Christopher Gamble 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the Christopher Gamble nor the names of its 12 | # contributors may be used to endorse or promote products derived 13 | # from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 19 | # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 20 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 23 | # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 24 | # OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | from twisted.internet import reactor 27 | from hashlib import md5 28 | from string import digits 29 | 30 | def _isHixie75(request): 31 | return request.getHeader("Sec-WebSocket-Version") is None and \ 32 | request.getHeader("Sec-WebSocket-Key1") is None and \ 33 | request.getHeader("Sec-WebSocket-Key2") is None 34 | 35 | def _isHybi00(request): 36 | return request.getHeader("Sec-WebSocket-Key1") is not None and \ 37 | request.getHeader("Sec-WebSocket-Key2") is not None 38 | 39 | def _challenge(key1, key2, challenge): 40 | first = int("".join(i for i in key1 if i in digits)) / key1.count(" ") 41 | second = int("".join(i for i in key2 if i in digits)) / key2.count(" ") 42 | nonce = md5(pack(">II8s", first, second, challenge)).digest() 43 | return nonce 44 | 45 | # ============================================================================================================ 46 | # === THIS IS A MODIFIED COPY OF twisted.web.websockets TO BE COMPATIBLE WITH OLDER VERSIONS OF WEBSOCKETS === 47 | # === IT WILL BE REMOVED WHEN SOCKJS STOPS NEEDING TO SUPPORT OLD, DUMB VERSIONS OF WEBSOCKETS === 48 | # ============================================================================================================ 49 | 50 | # -*- test-case-name: twisted.web.test.test_websockets -*- 51 | # Copyright (c) Twisted Matrix Laboratories. 52 | # 2011-2012 Oregon State University Open Source Lab 53 | # 2011-2012 Corbin Simpson 54 | # 55 | # See LICENSE for details. 56 | 57 | """ 58 | The WebSockets protocol (RFC 6455), provided as a resource which wraps a 59 | factory. 60 | """ 61 | 62 | __all__ = ["OldWebSocketsResource"] 63 | 64 | from hashlib import sha1 65 | from struct import pack, unpack 66 | 67 | from zope.interface import implementer, Interface 68 | 69 | from twisted.protocols.policies import ProtocolWrapper, WrappingFactory 70 | from twisted.python import log 71 | from twisted.python.constants import NamedConstant, Names 72 | from twisted.web.resource import IResource 73 | from twisted.web.server import NOT_DONE_YET 74 | 75 | 76 | 77 | class _WSException(Exception): 78 | """ 79 | Internal exception for control flow inside the WebSockets frame parser. 80 | """ 81 | 82 | 83 | 84 | # Control frame specifiers. Some versions of WS have control signals sent 85 | # in-band. Adorable, right? 86 | 87 | class _CONTROLS(Names): 88 | """ 89 | Control frame specifiers. 90 | """ 91 | 92 | NORMAL = NamedConstant() 93 | CLOSE = NamedConstant() 94 | PING = NamedConstant() 95 | PONG = NamedConstant() 96 | 97 | 98 | _opcodeTypes = { 99 | 0x0: _CONTROLS.NORMAL, 100 | 0x1: _CONTROLS.NORMAL, 101 | 0x2: _CONTROLS.NORMAL, 102 | 0x8: _CONTROLS.CLOSE, 103 | 0x9: _CONTROLS.PING, 104 | 0xa: _CONTROLS.PONG} 105 | 106 | 107 | _opcodeForType = { 108 | _CONTROLS.NORMAL: 0x1, 109 | _CONTROLS.CLOSE: 0x8, 110 | _CONTROLS.PING: 0x9, 111 | _CONTROLS.PONG: 0xa} 112 | 113 | 114 | # Authentication for WS. 115 | 116 | # The GUID for WebSockets, from RFC 6455. 117 | _WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 118 | 119 | 120 | 121 | def _makeAccept(key): 122 | """ 123 | Create an "accept" response for a given key. 124 | 125 | This dance is expected to somehow magically make WebSockets secure. 126 | 127 | @type key: C{str} 128 | @param key: The key to respond to. 129 | 130 | @rtype: C{str} 131 | @return: An encoded response. 132 | """ 133 | return sha1("%s%s" % (key, _WS_GUID)).digest().encode("base64").strip() 134 | 135 | 136 | 137 | # Frame helpers. 138 | # Separated out to make unit testing a lot easier. 139 | # Frames are bonghits in newer WS versions, so helpers are appreciated. 140 | 141 | 142 | 143 | def _mask(buf, key): 144 | """ 145 | Mask or unmask a buffer of bytes with a masking key. 146 | 147 | @type buf: C{str} 148 | @param buf: A buffer of bytes. 149 | 150 | @type key: C{str} 151 | @param key: The masking key. Must be exactly four bytes. 152 | 153 | @rtype: C{str} 154 | @return: A masked buffer of bytes. 155 | """ 156 | 157 | # This is super-secure, I promise~ 158 | key = [ord(i) for i in key] 159 | buf = list(buf) 160 | for i, char in enumerate(buf): 161 | buf[i] = chr(ord(char) ^ key[i % 4]) 162 | return "".join(buf) 163 | 164 | 165 | 166 | def _makeFrame(buf, old, _opcode=_CONTROLS.NORMAL): 167 | """ 168 | Make a frame. 169 | 170 | This function always creates unmasked frames, and attempts to use the 171 | smallest possible lengths. 172 | 173 | @type buf: C{str} 174 | @param buf: A buffer of bytes. 175 | 176 | @type _opcode: C{_CONTROLS} 177 | @param _opcode: Which type of frame to create. 178 | 179 | @rtype: C{str} 180 | @return: A packed frame. 181 | """ 182 | if old: 183 | if _opcode != _CONTROLS.NORMAL: 184 | return None 185 | return "\x00{0}\xFF".format(buf) 186 | else: 187 | bufferLength = len(buf) 188 | 189 | if bufferLength > 0xffff: 190 | length = "\x7f%s" % pack(">Q", bufferLength) 191 | elif bufferLength > 0x7d: 192 | length = "\x7e%s" % pack(">H", bufferLength) 193 | else: 194 | length = chr(bufferLength) 195 | 196 | # Always make a normal packet. 197 | header = chr(0x80 | _opcodeForType[_opcode]) 198 | frame = "%s%s%s" % (header, length, buf) 199 | return frame 200 | 201 | 202 | 203 | def _parseFrames(buf, old): 204 | """ 205 | Parse frames in a highly compliant manner. 206 | 207 | @type buf: C{str} 208 | @param buf: A buffer of bytes. 209 | 210 | @rtype: C{list} 211 | @return: A list of frames. 212 | """ 213 | if old: 214 | start = buf.find("\x00") 215 | tail = 0 216 | frames = [] 217 | while start != -1: 218 | end = buf.find("\xFF",start+1) 219 | if end == -1: 220 | break 221 | frame = buf[start+1:end] 222 | frames.append((_CONTROLS.NORMAL, frame)) 223 | tail = end + 1 224 | start = buf.find("\x00", tail) 225 | return frames, buf[tail:] 226 | else: 227 | start = 0 228 | frames = [] 229 | 230 | while True: 231 | # If there's not at least two bytes in the buffer, bail. 232 | if len(buf) - start < 2: 233 | break 234 | 235 | # Grab the header. This single byte holds some flags nobody cares 236 | # about, and an opcode which nobody cares about. 237 | header = ord(buf[start]) 238 | if header & 0x70: 239 | # At least one of the reserved flags is set. Pork chop sandwiches! 240 | raise _WSException("Reserved flag in frame (%d)" % header) 241 | 242 | # Get the opcode, and translate it to a local enum which we actually 243 | # care about. 244 | opcode = header & 0xf 245 | try: 246 | opcode = _opcodeTypes[opcode] 247 | except KeyError: 248 | raise _WSException("Unknown opcode %d in frame" % opcode) 249 | 250 | # Get the payload length and determine whether we need to look for an 251 | # extra length. 252 | length = ord(buf[start + 1]) 253 | masked = length & 0x80 254 | length &= 0x7f 255 | 256 | # The offset we're gonna be using to walk through the frame. We use 257 | # this because the offset is variable depending on the length and 258 | # mask. 259 | offset = 2 260 | 261 | # Extra length fields. 262 | if length == 0x7e: 263 | if len(buf) - start < 4: 264 | break 265 | 266 | length = buf[start + 2:start + 4] 267 | length = unpack(">H", length)[0] 268 | offset += 2 269 | elif length == 0x7f: 270 | if len(buf) - start < 10: 271 | break 272 | 273 | # Protocol bug: The top bit of this long long *must* be cleared; 274 | # that is, it is expected to be interpreted as signed. That's 275 | # fucking stupid, if you don't mind me saying so, and so we're 276 | # interpreting it as unsigned anyway. If you wanna send exabytes 277 | # of data down the wire, then go ahead! 278 | length = buf[start + 2:start + 10] 279 | length = unpack(">Q", length)[0] 280 | offset += 8 281 | 282 | if masked: 283 | if len(buf) - (start + offset) < 4: 284 | # This is not strictly necessary, but it's more explicit so 285 | # that we don't create an invalid key. 286 | break 287 | 288 | key = buf[start + offset:start + offset + 4] 289 | offset += 4 290 | 291 | if len(buf) - (start + offset) < length: 292 | break 293 | 294 | data = buf[start + offset:start + offset + length] 295 | 296 | if masked: 297 | data = _mask(data, key) 298 | 299 | if opcode == _CONTROLS.CLOSE: 300 | if len(data) >= 2: 301 | # Gotta unpack the opcode and return usable data here. 302 | data = unpack(">H", data[:2])[0], data[2:] 303 | else: 304 | # No reason given; use generic data. 305 | data = 1000, "No reason given" 306 | 307 | frames.append((opcode, data)) 308 | start += offset + length 309 | 310 | return frames, buf[start:] 311 | 312 | 313 | 314 | class _WebSocketsProtocol(ProtocolWrapper): 315 | """ 316 | Protocol which wraps another protocol to provide a WebSockets transport 317 | layer. 318 | """ 319 | _buffer = None 320 | challenge = None 321 | connected = False 322 | pending_dc = False 323 | 324 | def __init__(self, *args, **kwargs): 325 | ProtocolWrapper.__init__(self, *args, **kwargs) 326 | self._pending_frames = [] 327 | 328 | def connectionMade(self): 329 | """ 330 | Log the new connection and initialize the buffer list. 331 | """ 332 | connected = True 333 | if not self.challenge: 334 | ProtocolWrapper.connectionMade(self) 335 | log.msg("Opening connection with %s" % self.transport.getPeer()) 336 | self._buffer = [] 337 | 338 | 339 | def _parseFrames(self): 340 | """ 341 | Find frames in incoming data and pass them to the underlying protocol. 342 | """ 343 | try: 344 | frames, rest = _parseFrames("".join(self._buffer), self.old) 345 | except _WSException: 346 | # Couldn't parse all the frames, something went wrong, let's bail. 347 | log.err() 348 | self.loseConnection() 349 | return 350 | 351 | self._buffer[:] = [rest] 352 | 353 | for frame in frames: 354 | opcode, data = frame 355 | if opcode == _CONTROLS.NORMAL: 356 | # Business as usual. Decode the frame, if we have a decoder. 357 | # Pass the frame to the underlying protocol. 358 | ProtocolWrapper.dataReceived(self, data) 359 | elif opcode == _CONTROLS.CLOSE: 360 | # The other side wants us to close. I wonder why? 361 | reason, text = data 362 | log.msg("Closing connection: %r (%d)" % (text, reason)) 363 | 364 | # Close the connection. 365 | self.transport.loseConnection() 366 | return 367 | elif opcode == _CONTROLS.PING: 368 | # 5.5.2 PINGs must be responded to with PONGs. 369 | # 5.5.3 PONGs must contain the data that was sent with the 370 | # provoking PING. 371 | self.transport.write(_makeFrame(data, self.old, _opcode=_CONTROLS.PONG)) 372 | 373 | 374 | def _sendFrames(self): 375 | """ 376 | Send all pending frames. 377 | 378 | @param frames: A list of byte strings to send. 379 | @type frames: C{list} 380 | """ 381 | # Don't send anything before the challenge 382 | if self.challenge: 383 | return 384 | for frame in self._pending_frames: 385 | packet = _makeFrame(frame, self.old) 386 | self.transport.write(packet) 387 | self._pending_frames = [] 388 | 389 | 390 | def dataReceived(self, data): 391 | """ 392 | Append the data to the buffer list and parse the whole. 393 | """ 394 | self._buffer.append(data) 395 | 396 | if self.challenge: 397 | buf = "".join(self._buffer) 398 | if len(buf) >= 8: 399 | challenge, buf = buf[:8], buf[8:] 400 | self._buffer = [buf] 401 | nonce = self.challenge(challenge) 402 | self.transport.write(nonce) 403 | self.challenge = None 404 | if self.connected: 405 | ProtocolWrapper.connectionMade(self) 406 | self.dataReceived("") # Kick it off proper 407 | if self.pending_dc: 408 | self.pending_dc = False 409 | self.loseConnection() 410 | else: 411 | self._parseFrames() 412 | if self._pending_frames: 413 | self._sendFrames() 414 | 415 | self._parseFrames() 416 | 417 | 418 | def write(self, data): 419 | """ 420 | Write to the transport. 421 | 422 | This method will only be called by the underlying protocol. 423 | """ 424 | self._pending_frames.append(data) 425 | self._sendFrames() 426 | 427 | 428 | def writeSequence(self, data): 429 | """ 430 | Write a sequence of data to the transport. 431 | 432 | This method will only be called by the underlying protocol. 433 | """ 434 | self._pending_frames.extend(data) 435 | self._sendFrames() 436 | 437 | 438 | def loseConnection(self): 439 | """ 440 | Close the connection. 441 | 442 | This includes telling the other side we're closing the connection. 443 | 444 | If the other side didn't signal that the connection is being closed, 445 | then we might not see their last message, but since their last message 446 | should, according to the spec, be a simple acknowledgement, it 447 | shouldn't be a problem. 448 | """ 449 | # Send a closing frame. It's only polite. (And might keep the browser 450 | # from hanging.) 451 | if not self.disconnecting: 452 | if not self.challenge: 453 | self.disconnecting = True 454 | frame = _makeFrame("", self.old, _opcode=_CONTROLS.CLOSE) 455 | if frame: 456 | self.transport.write(frame) 457 | else: 458 | self.transport.loseConnection() 459 | else: 460 | self.pending_dc = True 461 | 462 | 463 | 464 | class _WebSocketsFactory(WrappingFactory): 465 | """ 466 | Factory which wraps another factory to provide WebSockets frames for all 467 | of its protocols. 468 | 469 | This factory does not provide the HTTP headers required to perform a 470 | WebSockets handshake; see C{WebSocketsResource}. 471 | """ 472 | protocol = _WebSocketsProtocol 473 | 474 | 475 | 476 | class IWebSocketsResource(Interface): 477 | """ 478 | A WebSockets resource. 479 | 480 | @since: 13.0 481 | """ 482 | 483 | def lookupProtocol(protocolNames, request): 484 | """ 485 | Build a protocol instance for the given protocol options and request. 486 | 487 | @param protocolNames: The asked protocols from the client. 488 | @type protocolNames: C{list} of C{str} 489 | 490 | @param request: The connecting client request. 491 | @type request: L{IRequest} 492 | 493 | @return: A tuple of (protocol, C{None}). 494 | @rtype: C{tuple} 495 | """ 496 | 497 | 498 | 499 | @implementer(IResource, IWebSocketsResource) 500 | class OldWebSocketsResource(object): 501 | """ 502 | A resource for serving a protocol through WebSockets. 503 | 504 | This class wraps a factory and connects it to WebSockets clients. Each 505 | connecting client will be connected to a new protocol of the factory. 506 | 507 | Due to unresolved questions of logistics, this resource cannot have 508 | children. 509 | 510 | @since: 13.0 511 | """ 512 | isLeaf = True 513 | 514 | def __init__(self, factory): 515 | self._oldfactory = _WebSocketsFactory(factory) 516 | 517 | 518 | def getChildWithDefault(self, name, request): 519 | """ 520 | Reject attempts to retrieve a child resource. All path segments beyond 521 | the one which refers to this resource are handled by the WebSocket 522 | connection. 523 | """ 524 | raise RuntimeError( 525 | "Cannot get IResource children from WebsocketsResourceTest") 526 | 527 | 528 | def putChild(self, path, child): 529 | """ 530 | Reject attempts to add a child resource to this resource. The 531 | WebSocket connection handles all path segments beneath this resource, 532 | so L{IResource} children can never be found. 533 | """ 534 | raise RuntimeError( 535 | "Cannot put IResource children under WebSocketsResource") 536 | 537 | 538 | def lookupProtocol(self, protocolNames, request): 539 | """ 540 | Build a protocol instance for the given protocol options and request. 541 | This default implementation ignores the protocols and just return an 542 | instance of protocols built by C{self._oldfactory}. 543 | 544 | @param protocolNames: The asked protocols from the client. 545 | @type protocolNames: C{list} of C{str} 546 | 547 | @param request: The connecting client request. 548 | @type request: L{Request} 549 | 550 | @return: A tuple of (protocol, C{None}). 551 | @rtype: C{tuple} 552 | """ 553 | protocol = self._oldfactory.buildProtocol(request.transport.getPeer()) 554 | return protocol, None 555 | 556 | 557 | def render(self, request): 558 | """ 559 | Render a request. 560 | 561 | We're not actually rendering a request. We are secretly going to handle 562 | a WebSockets connection instead. 563 | 564 | @param request: The connecting client request. 565 | @type request: L{Request} 566 | 567 | @return: a strinf if the request fails, otherwise C{NOT_DONE_YET}. 568 | """ 569 | request.defaultContentType = None 570 | # If we fail at all, we're gonna fail with 400 and no response. 571 | # You might want to pop open the RFC and read along. 572 | failed = False 573 | 574 | if request.method != "GET": 575 | # 4.2.1.1 GET is required. 576 | failed = True 577 | 578 | upgrade = request.getHeader("Upgrade") 579 | if upgrade is None or "websocket" not in upgrade.lower(): 580 | # 4.2.1.3 Upgrade: WebSocket is required. 581 | failed = True 582 | 583 | connection = request.getHeader("Connection") 584 | if connection is None or "upgrade" not in connection.lower(): 585 | # 4.2.1.4 Connection: Upgrade is required. 586 | failed = True 587 | 588 | ##key = request.getHeader("Sec-WebSocket-Key") 589 | ##if key is None: 590 | ## # 4.2.1.5 The challenge key is required. 591 | ## failed = True 592 | 593 | ##version = request.getHeader("Sec-WebSocket-Version") 594 | ##if version != "13": 595 | ## # 4.2.1.6 Only version 13 works. 596 | ## failed = True 597 | ## # 4.4 Forward-compatible version checking. 598 | ## request.setHeader("Sec-WebSocket-Version", "13") 599 | 600 | if failed: 601 | request.setResponseCode(400) 602 | return "" 603 | 604 | askedProtocols = request.requestHeaders.getRawHeaders( 605 | "Sec-WebSocket-Protocol") 606 | protocol, protocolName = self.lookupProtocol(askedProtocols, request, True) 607 | 608 | # If a protocol is not created, we deliver an error status. 609 | if not protocol.wrappedProtocol: 610 | request.setResponseCode(502) 611 | return "" 612 | 613 | # We are going to finish this handshake. We will return a valid status 614 | # code. 615 | # 4.2.2.5.1 101 Switching Protocols 616 | request.setResponseCode(101) 617 | # 4.2.2.5.2 Upgrade: websocket 618 | request.setHeader("Upgrade", "WebSocket") 619 | # 4.2.2.5.3 Connection: Upgrade 620 | request.setHeader("Connection", "Upgrade") 621 | ## This is a big mess of setting various headers based on which version we are 622 | ## And determining whether to use "old frames" or "new frames" 623 | if _isHixie75(request) or _isHybi00(request): 624 | protocol.old = True 625 | host = request.getHeader("Host") or "example.com" 626 | origin = request.getHeader("Origin") or "http://example.com" 627 | location = "{0}://{1}{2}".format("wss" if request.isSecure() else "ws", host, request.path) 628 | if _isHixie75(request): 629 | request.setHeader("WebSocket-Origin", origin) 630 | request.setHeader("WebSocket-Location", location) 631 | if protocolName: 632 | request.setHeader("WebSocket-Protocol", protocolName) 633 | else: 634 | protocol.challenge = lambda x: _challenge(request.getHeader("Sec-WebSocket-Key1"), request.getHeader("Sec-WebSocket-Key2"), x) 635 | request.setHeader("Sec-WebSocket-Origin", origin) 636 | request.setHeader("Sec-WebSocket-Location", location) 637 | if protocolName: 638 | request.setHeader("Sec-WebSocket-Protocol", protocolName) 639 | else: 640 | protocol.old = False 641 | key = request.getHeader("Sec-WebSocket-Key") 642 | if key is None: 643 | request.setResponseCode(400) 644 | return "" 645 | version = request.getHeader("Sec-WebSocket-Version") 646 | if version not in ("7","8","13"): 647 | request.setResponseCode(400) 648 | request.setHeader("Sec-WebSocket-Version", "13") 649 | return "" 650 | request.setHeader("Sec-WebSocket-Version", version) 651 | request.setHeader("Sec-WebSocket-Accept", _makeAccept(key)) 652 | if protocolName: 653 | request.setHeader("Sec-WebSocket-Protocol", protocolName) 654 | 655 | # Provoke request into flushing headers and finishing the handshake. 656 | request.write("") 657 | 658 | # And now take matters into our own hands. We shall manage the 659 | # transport's lifecycle. 660 | transport, request.transport = request.transport, None 661 | 662 | # Connect the transport to our factory, and make things go. We need to 663 | # do some stupid stuff here; see #3204, which could fix it. 664 | if request.isSecure(): 665 | # Secure connections wrap in TLSMemoryBIOProtocol too. 666 | transport.protocol.wrappedProtocol = protocol 667 | else: 668 | transport.protocol = protocol 669 | protocol.makeConnection(transport) 670 | 671 | ## Copy the buffer 672 | protocol.dataReceived(request.channel.clearLineBuffer()) 673 | 674 | return NOT_DONE_YET 675 | --------------------------------------------------------------------------------