├── 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 |20 |
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 |
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 |
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 |
31 |
32 | Latency:
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 | 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 | 44 | 45 | Latency:
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 | Expected: | '+c+" |
|---|---|
| Result: | '+b+" |
| Diff: | '+e.diff(c,b)+" |
| Source: | '+j(k)+" |