Commit 85566b3b authored by root's avatar root

Add sockjs for python3

parent 46f2f5eb
# Auto detect text files and perform LF normalization
* text=auto
# Custom for Visual Studio
*.cs diff=csharp
*.sln merge=union
*.csproj merge=union
*.vbproj merge=union
*.fsproj merge=union
*.dbproj merge=union
# Standard to msysgit
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
#############
## Python
#############
*.py[co]
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
#Translations
*.mo
#Mr Developer
.mr.developer.cfg
# Mac crap
.DS_Store
Christopher Gamble
\ No newline at end of file
========================
SockJS-Twisted Changelog
========================
0.1
===
**0.1.1**
* Converts all text to UTF-8 (prevents crashing websockets in chrome)
* Minor bug fixes
**0.1.0**
* Initial release
Copyright (c) 2012, Christopher Gamble
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the Christopher Gamble nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
include AUTHORS
include LICENSE
include README.rst
\ No newline at end of file
==============
SockJS-Twisted
==============
A simple library for adding SockJS support to your twisted application.
Status
======
SockJS-Twisted passes all `SockJS-Protocol v0.3.3 <https://github.com/sockjs/sockjs-protocol>`_ tests,
and all `SockJS-Client qunit <https://github.com/sockjs/sockjs-client>`_ tests. It has been used in
production environments, and should be free of any critical bugs.
Usage
=====
Use ``txsockjs.factory.SockJSFactory`` to wrap your factories. That's it!
.. code-block:: python
from txsockjs.factory import SockJSFactory
reactor.listenTCP(8080, SockJSFactory(factory_to_wrap))
There is nothing else to it, no special setup involved.
Do you want a secure connection? Use ``listenSSL()`` instead of ``listenTCP()``.
Advanced Usage
==============
For those who want to host multiple SockJS services off of one port,
``txsockjs.factory.SockJSMultiFactory`` is designed to handle routing for you.
.. code-block:: python
from txsockjs.factory import SockJSMultiFactory
f = SockJSMultiFactory()
f.addFactory(EchoFactory(), "echo")
f.addFactory(ChatFactory(), "chat")
reactor.listenTCP(8080, f)
http://localhost:8080/echo and http://localhost:8080/chat will give you access
to your EchoFactory and ChatFactory.
Integration With Websites
=========================
It is possible to offer static resources, dynamic pages, and SockJS endpoints off of
a single port by using ``txsockjs.factory.SockJSResource``.
.. code-block:: python
from txsockjs.factory import SockJSResource
root = resource.Resource()
root.putChild("echo", SockJSResource(EchoFactory()))
root.putChild("chat", SockJSResource(ChatFactory()))
site = server.Site(root)
reactor.listenTCP(8080, site)
Multiplexing [Experimental]
===========================
SockJS-Twisted also has built-in support for multiplexing. See the
`Websocket-Multiplex <https://github.com/sockjs/websocket-multiplex>`_ library
for how to integrate multiplexing client side.
.. code-block:: python
from txsockjs.multiplex import SockJSMultiplexResource
multiplex = SockJSMultiplexResource()
multiplex.addFactory("echo", EchoFactory())
multiplex.addFactory("chat", ChatFactory())
root = resource.Resource()
root.putChild("multiplex", multiplex)
site = server.Site(root)
reactor.listenTCP(8080, site)
If you want PubSub functionality, just use ``txsockjs.multiplex.SockJSPubSubResource`` instead!
Options
=======
A dictionary of options can be passed into the factory to control SockJS behavior.
.. code-block:: python
options = {
'websocket': True,
'cookie_needed': False,
'heartbeat': 25,
'timeout': 5,
'streaming_limit': 128 * 1024,
'encoding': 'cp1252', # Latin1
'sockjs_url': 'https://d1fxtkz8shb9d2.cloudfront.net/sockjs-0.3.js'
}
SockJSFactory(factory_to_wrap, options)
SockJSMultiFactory().addFactory(factory_to_wrap, prefix, options)
SockJSResource(factory_to_wrap, options)
SockJSMultiplexResource(options)
SockJSPubSubResource(options)
websocket :
whether websockets are supported as a protocol. Useful for proxies or load balancers that don't support websockets.
cookie_needed :
whether the JSESSIONID cookie is set. Results in less performant protocols being used, so don't require them unless your load balancer requires it.
heartbeat :
how often a heartbeat message is sent to keep the connection open. Do not increase this unless you know what you are doing.
timeout :
maximum delay between connections before the underlying protocol is disconnected
streaming_limit :
how many bytes can be sent over a streaming protocol before it is cycled. Allows browser-side garbage collection to lower RAM usage.
encoding :
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.
sockjs_url :
The url of the SockJS library to use in iframes. By default this is served over HTTPS and therefore shouldn't need changing.
License
=======
SockJS-Twisted is (c) 2012 Christopher Gamble and is made available under the BSD license.
from setuptools import setup
import txsockjs
import os
setup(
author="Christopher Gamble",
author_email="chris@chrisgamble.net",
name="txsockjs",
version=txsockjs.__version__,
description="Twisted SockJS wrapper",
long_description=open(os.path.join(os.path.dirname(__file__), 'README.rst')).read(),
url="http://github.com/Fugiman/sockjs-twisted",
license='BSD License',
platforms=['OS Independent'],
packages=["txsockjs","txsockjs.protocols"],
install_requires=[
"Twisted",
],
classifiers=[
"Development Status :: 5 - Production/Stable",
"Framework :: Twisted",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Topic :: Internet",
],
)
\ No newline at end of file
<!doctype html>
<html>
<head>
<title>IRC Client Test</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
<style>
#container {
width: 800px;
margin: 40px auto;
padding: 0;
}
#irc-holder {
height: 600px;
width: 800px;
overflow: auto;
border: 2px solid black;
}
#irc { width: 100%; border-collapse: collapse; }
#irc tr { border-bottom: 1px solid gray; }
#chatter input[type="text"] { width: 700px; }
#chatter input[type="submit"] { width: 90px; }
#protocol { font-size: 1.5em; margin: auto; width: 300px; display: block; }
</style>
</head>
<body>
<div id="container">
<div id="irc-holder">
<table id="irc"></table>
</div>
<form method="GET" id="chatter">
<input type="text" />
<input type="submit" value="Send" />
<br />
<select id="protocol">
<option value="websocket" selected>Websocket</option>
<option value="xdr-streaming">XDR Streaming</option>
<option value="xhr-streaming">XHR Streaming</option>
<option value="iframe-eventsource">Eventsource</option>
<option value="iframe-htmlfile">HTMLFile</option>
<option value="xdr-polling">XDR Polling</option>
<option value="xhr-polling">XHR Polling</option>
<option value="iframe-xhr-polling">XHR Polling (IFrame)</option>
<option value="jsonp-polling">JSONP Polling</option>
</select>
</form>
</div>
<script>
$(document).keypress(function(e) { return e.which !== 0; });
var url = 'http://127.0.0.1:6672';
var conn = false;
function connect() {
if(conn)
conn.close()
conn = false
var protocol = $('#protocol').val();
conn = new SockJS(url,null,{debug:true,devel:true,protocols_whitelist:[protocol]});
var log = function(msg) { $('#irc').append($('<tr>').html($('<td>').html(msg))); };
conn.onopen = function(evt) { log('<i>Connected via '+$('#protocol').val()+'</i>'); };
conn.onclose = function(evt) { log('<i>Disconnected</i>'); };
conn.onmessage = function(evt) { log($('<div>').text(evt.data).html()); };
}
$('#chatter').submit(function() {
var txt = $(this).find('input[type="text"]');
var m = txt.val();
conn.send(m+"\r\n");
txt.val('');
return false;
});
$('#protocol').change(connect);
connect();
</script>
</body>
</html>
from twisted.internet.protocol import Factory
from twisted.protocols.basic import LineReceiver
from twisted.internet import reactor
from txsockjs.factory import SockJSFactory
class Chat(LineReceiver):
def __init__(self, users):
self.users = users
self.name = None
self.state = "GETNAME"
def connectionMade(self):
print("IRC Connection Made!")
self.sendLine("What's your name?")
def connectionLost(self, reason):
print("IRC Connection Lost!")
if self.users.has_key(self.name):
del self.users[self.name]
def lineReceived(self, line):
if self.state == "GETNAME":
self.handle_GETNAME(line)
else:
self.handle_CHAT(line)
def handle_GETNAME(self, name):
if self.users.has_key(name):
self.sendLine("Name taken, please choose another.")
return
print("IRC User chose name - %s!" % name)
self.sendLine("Welcome, %s!" % (name,))
self.name = name
self.users[name] = self
self.state = "CHAT"
def handle_CHAT(self, message):
message = "<%s> %s" % (self.name, message)
print(message)
for name, protocol in self.users.iteritems():
protocol.sendLine(message)
class ChatFactory(Factory):
def __init__(self):
self.users = {} # maps user names to Chat instances
def buildProtocol(self, addr):
return Chat(self.users)
f = ChatFactory()
s = SockJSFactory(f)
reactor.listenTCP(6667, f)
reactor.listenTCP(6672, s)
reactor.run()
\ No newline at end of file
<!doctype html>
<html><head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
<script src="http://cdn.sockjs.org/websocket-multiplex-0.1.js"></script>
<style>
.box {
width: 300px;
float: left;
margin: 0 20px 0 20px;
}
.box div, .box input {
border: 1px solid;
-moz-border-radius: 4px;
border-radius: 4px;
width: 100%;
padding: 0px;
margin: 5px;
}
.box div {
border-color: grey;
height: 300px;
overflow: auto;
}
.box input {
height: 30px;
}
h1 {
margin-left: 75px;
}
body {
background-color: #F0F0F0;
font-family: "Arial";
}
</style>
</head><body lang="en">
<h1>SockJS Multiplex example</h1>
<div id="first" class="box">
<div></div>
<form><input autocomplete="off" value="Type here..."></input></form>
</div>
<div id="second" class="box">
<div></div>
<form><input autocomplete="off"></input></form>
</div>
<div id="third" class="box">
<div></div>
<form><input autocomplete="off"></input></form>
</div>
<script>
// Pipe - convenience wrapper to present data received from an
// object supporting WebSocket API in an html element. And the other
// direction: data typed into an input box shall be sent back.
var pipe = function(ws, el_name) {
var div = $(el_name + ' div');
var inp = $(el_name + ' input');
var form = $(el_name + ' form');
var print = function(m, p) {
p = (p === undefined) ? '' : JSON.stringify(p);
div.append($("<code>").text(m + ' ' + p));
div.append($("<br>"));
div.scrollTop(div.scrollTop() + 10000);
};
ws.onopen = function() {print('[*] open', ws.protocol);};
ws.onmessage = function(e) {print('[.] message', e.data);};
ws.onclose = function() {print('[*] close');};
form.submit(function() {
print('[ ] sending', inp.val());
ws.send(inp.val());
inp.val('');
return false;
});
};
var sockjs_url = 'http://127.0.0.1:8081/multiplex';
var sockjs = new SockJS(sockjs_url);
var multiplexer = new WebSocketMultiplex(sockjs);
var ann = multiplexer.channel('ann');
var bob = multiplexer.channel('bob');
var carl = multiplexer.channel('carl');
pipe(ann, '#first');
pipe(bob, '#second');
pipe(carl, '#third');
$('#first input').focus();
</script>
</body></html>
\ No newline at end of file
from twisted.internet import reactor, protocol
from twisted.web import server, resource
from txsockjs.multiplex import SockJSMultiplexResource
class AnnP(protocol.Protocol):
def connectionMade(self):
self.transport.write("Ann says hi!")
def dataReceived(self, data):
self.transport.write("Ann nods: " + data)
class BobP(protocol.Protocol):
def connectionMade(self):
self.transport.write("Bob doesn't agree.")
def dataReceived(self, data):
self.transport.write("Bob says no to: " + data)
class CarlP(protocol.Protocol):
def connectionMade(self):
self.transport.write("Carl says goodbye!")
self.transport.loseConnection()
class AnnF(protocol.Factory):
protocol = AnnP
class BobF(protocol.Factory):
protocol = BobP
class CarlF(protocol.Factory):
protocol = CarlP
multiplex = SockJSMultiplexResource()
multiplex.addFactory("ann", AnnF())
multiplex.addFactory("bob", BobF())
multiplex.addFactory("carl", CarlF())
root = resource.Resource()
root.putChild("multiplex", multiplex)
site = server.Site(root)
reactor.listenTCP(8081, site)
reactor.run()
\ No newline at end of file
from twisted.internet import reactor, protocol
from twisted.web import server, resource
from txsockjs.factory import SockJSMultiFactory, SockJSResource
class Echo(protocol.Protocol):
def dataReceived(self, data):
#print ">>> %s" % data
self.transport.write(data)
class EchoFactory(protocol.Factory):
def buildProtocol(self, addr):
return Echo()
class Close(protocol.Protocol):
def connectionMade(self):
self.transport.loseConnection()
class CloseFactory(protocol.Factory):
def buildProtocol(self, addr):
return Close()
echo = EchoFactory()
close = CloseFactory()
s = SockJSMultiFactory()
s.addFactory(echo, "echo", {'streaming_limit': 4 * 1024})
s.addFactory(close, "close", {'streaming_limit': 4 * 1024})
s.addFactory(echo, "disabled_websocket_echo", {'websocket': False, 'streaming_limit': 4 * 1024})
s.addFactory(echo, "cookie_needed_echo", {'cookie_needed': True, 'streaming_limit': 4 * 1024})
root = resource.Resource()
root.putChild("echo", SockJSResource(echo, {'streaming_limit': 4 * 1024}))
root.putChild("close", SockJSResource(close, {'streaming_limit': 4 * 1024}))
root.putChild("disabled_websocket_echo", SockJSResource(echo, {'websocket': False, 'streaming_limit': 4 * 1024}))
root.putChild("cookie_needed_echo", SockJSResource(echo, {'cookie_needed': True, 'streaming_limit': 4 * 1024}))
site = server.Site(root)
reactor.listenTCP(8081, s)
reactor.listenTCP(8082, site)
reactor.run()
\ No newline at end of file
# Copyright (c) 2012, Christopher Gamble
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the Christopher Gamble nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
from twisted.web import resource, server
from txsockjs.protocols.base import Stub
from txsockjs.protocols.eventsource import EventSource
from txsockjs.protocols.htmlfile import HTMLFile
from txsockjs.protocols.jsonp import JSONP, JSONPSend
from txsockjs.protocols.static import Info, IFrame
from txsockjs.protocols.websocket import RawWebSocket, WebSocket
from txsockjs.protocols.xhr import XHR, XHRSend, XHRStream
class SockJSFactory(server.Site):
def __init__(self, factory, options = None):
server.Site.__init__(self, SockJSResource(factory, options))
class SockJSMultiFactory(server.Site):
def __init__(self):
server.Site.__init__(self, resource.Resource())
def addFactory(self, factory, prefix, options = None):
self.resource.putChild(prefix, SockJSResource(factory, options))
class SockJSResource(resource.Resource):
def __init__(self, factory, options = None):
resource.Resource.__init__(self)
self._factory = factory
self._sessions = {}
self._options = {
'websocket': True,
'cookie_needed': False,
'heartbeat': 25,
'timeout': 5,
'streaming_limit': 128 * 1024,
'encoding': 'cp1252', #Latin1
'sockjs_url': 'https://d1fxtkz8shb9d2.cloudfront.net/sockjs-0.3.js'
}
if options is not None:
self._options.update(options)
# Just in case somebody wants to mess with these
self._methods = {
'xhr': XHR,
'xhr_send': XHRSend,
'xhr_streaming': XHRStream,
'eventsource': EventSource,
'htmlfile': HTMLFile,
'jsonp': JSONP,
'jsonp_send': JSONPSend,
}
self._writeMethods = ('xhr_send','jsonp_send')
# Static Resources
self.putChild("info",Info())
self.putChild("iframe.html",IFrame())
self.putChild("websocket",RawWebSocket())
# Since it's constant, we can declare the websocket handler up here
self._websocket = WebSocket()
self._websocket.parent = self
def getChild(self, name, request):
# Check if it is the greeting url
if not name and not request.postpath:
return self
# Hacks to resove the iframe even when people are dumb
if len(name) > 10 and name[:6] == "iframe" and name[-5:] == ".html":
return self.children["iframe.html"]
# Sessions must have 3 parts, name is already the first. Also, no periods in the loadbalancer
if len(request.postpath) != 2 or "." in name or not name:
return resource.NoResource("No such child resource.")
# Extract session & request type. Discard load balancer
session, name = request.postpath
# No periods in the session
if "." in session or not session:
return resource.NoResource("No such child resource.")
# Websockets are a special case
if name == "websocket":
return self._websocket
# Reject invalid methods
if name not in self._methods:
return resource.NoResource("No such child resource.")
# Reject writes to invalid sessions, unless just checking options
if name in self._writeMethods and session not in self._sessions and request.method != "OPTIONS":
return resource.NoResource("No such child resource.")
# Generate session if doesn't exist, unless just checking options
if session not in self._sessions and request.method != "OPTIONS":
self._sessions[session] = Stub(self, session)
# Delegate request to appropriate handler
return self._methods[name](self, self._sessions[session] if request.method != "OPTIONS" else None)
def putChild(self, path, child):
child.parent = self
resource.Resource.putChild(self, path, child)
def setBaseHeaders(self, request, cookie=True):
origin = request.getHeader("Origin")
headers = request.getHeader('Access-Control-Request-Headers')
if origin is None or origin == "null":
origin = "*"
request.setHeader('access-control-allow-origin', origin)
request.setHeader('access-control-allow-credentials', 'true')
request.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
if headers is not None:
request.setHeader('Access-Control-Allow-Headers', headers)
if self._options["cookie_needed"] and cookie:
cookie = request.getCookie("JSESSIONID") if request.getCookie("JSESSIONID") else "dummy"
request.addCookie("JSESSIONID", cookie, path="/")
def render_GET(self, request):
self.setBaseHeaders(request,False)
request.setHeader('content-type', 'text/plain; charset=UTF-8')
return "Welcome to SockJS!\n"
# Copyright (c) 2012, Christopher Gamble
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the Christopher Gamble nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
from twisted.internet.protocol import Protocol, Factory
from twisted.protocols.policies import ProtocolWrapper
from txsockjs.factory import SockJSResource
from txsockjs import utils
class BroadcastProtocol(Protocol):
def dataReceived(self, data):
self.transport.broadcast(data)
class BroadcastFactory(Factory):
protocol = BroadcastProtocol
class MultiplexProxy(ProtocolWrapper):
def __init__(self, factory, wrappedProtocol, transport, topic):
ProtocolWrapper.__init__(self, factory, wrappedProtocol)
self.topic = topic
self.makeConnection(transport)
def write(self, data):
self.transport.transport.write(",".join(["msg", self.topic, data]))
def writeSequence(self, data):
for d in data:
self.write(d)
def broadcast(self, data):
self.factory.broadcast(self.topic, data)
def loseConnection(self):
self.transport.transport.write(",".join(["uns", self.topic]))
class MultiplexProtocol(Protocol):
def connectionMade(self):
self.factory._connections[self] = {}
def dataReceived(self, message):
type, chaff, topic = message.partition(",")
if "," in topic:
topic, chaff, payload = topic.partition(",")
if type == "sub":
self.factory.subscribe(self, topic)
elif type == "msg":
self.factory.handleMessage(self, topic, payload)
elif type == "uns":
self.factory.unsubscribe(self, topic)
def connectionLost(self, reason=None):
for conn in list(self.factory._connections[self].values()):
conn.connectionLost(reason)
del self.factory._connections[self]
class MultiplexFactory(Factory):
protocol = MultiplexProtocol
def __init__(self, resource):
self._resource = resource
self._topics = {}
self._connections = {}
def addFactory(self, name, factory):
self._topics[name] = factory
def broadcast(self, name, message):
targets = []
message = ",".join(["msg", name, message])
for p, topics in list(self._connections.items()):
if name in topics:
targets.append(p)
utils.broadcast(message, targets)
def removeFactory(self, name, factory):
del self._topics[name]
def subscribe(self, p, name):
if name not in self._topics:
return
self._connections[p][name] = MultiplexProxy(self, self._topics[name].buildProtocol(p.transport.getPeer()), p, name)
def handleMessage(self, p, name, message):
if p not in self._connections:
return
if name not in self._connections[p]:
return
self._connections[p][name].dataReceived(message)
def unsubscribe(self, p, name):
if p not in self._connections:
return
if name not in self._connections[p]:
return
self._connections[p][name].connectionLost(None)
del self._connections[p][name]
def registerProtocol(self, p):
pass
def unregisterProtocol(self, p):
pass
class PubSubFactory(MultiplexFactory):
broadcastFactory = BroadcastFactory()
def subscribe(self, p, name):
if name not in self._topics:
self._topics[name] = self.broadcastFactory
MultiplexFactory.subscribe(self, p, name)
class SockJSMultiplexResource(SockJSResource):
def __init__(self, options=None):
SockJSResource.__init__(self, MultiplexFactory(self), options)
def addFactory(self, name, factory):
return self._factory.addFactory(name, factory)
def broadcast(self, name, message):
return self._factory.broadcast(name, message)
def removeFactory(self, name):
return self._factory.removeFactory(name)
class SockJSPubSubResource(SockJSMultiplexResource):
def __init__(self, options=None):
SockJSResource.__init__(self, PubSubFactory(self), options)
# Copyright (c) 2012, Christopher Gamble
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the Christopher Gamble nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
from twisted.internet.protocol import Protocol, Factory
from twisted.protocols.policies import ProtocolWrapper
from txsockjs.factory import SockJSResource
from txsockjs import utils
class BroadcastProtocol(Protocol):
def dataReceived(self, data):
self.transport.broadcast(data)
class BroadcastFactory(Factory):
protocol = BroadcastProtocol
class MultiplexProxy(ProtocolWrapper):
def __init__(self, factory, wrappedProtocol, transport, topic):
ProtocolWrapper.__init__(self, factory, wrappedProtocol)
self.topic = topic
self.makeConnection(transport)
def write(self, data):
self.transport.transport.write(",".join(["msg", self.topic, data]))
def writeSequence(self, data):
for d in data:
self.write(d)
def broadcast(self, data):
self.factory.broadcast(self.topic, data)
def loseConnection(self):
self.transport.transport.write(",".join(["uns", self.topic]))
class MultiplexProtocol(Protocol):
def connectionMade(self):
self.factory._connections[self] = {}
def dataReceived(self, message):
type, chaff, topic = message.partition(",")
if "," in topic:
topic, chaff, payload = topic.partition(",")
if type == "sub":
self.factory.subscribe(self, topic)
elif type == "msg":
self.factory.handleMessage(self, topic, payload)
elif type == "uns":
self.factory.unsubscribe(self, topic)
def connectionLost(self, reason=None):
for conn in self.factory._connections[self].values():
conn.connectionLost(reason)
del self.factory._connections[self]
class MultiplexFactory(Factory):
protocol = MultiplexProtocol
def __init__(self, resource):
self._resource = resource
self._topics = {}
self._connections = {}
def addFactory(self, name, factory):
self._topics[name] = factory
def broadcast(self, name, message):
targets = []
message = ",".join(["msg", name, message])
for p, topics in self._connections.items():
if name in topics:
targets.append(p)
utils.broadcast(message, targets)
def removeFactory(self, name, factory):
del self._topics[name]
def subscribe(self, p, name):
if name not in self._topics:
return
self._connections[p][name] = MultiplexProxy(self, self._topics[name].buildProtocol(p.transport.getPeer()), p, name)
def handleMessage(self, p, name, message):
if p not in self._connections:
return
if name not in self._connections[p]:
return
self._connections[p][name].dataReceived(message)
def unsubscribe(self, p, name):
if p not in self._connections:
return
if name not in self._connections[p]:
return
self._connections[p][name].connectionLost(None)
del self._connections[p][name]
def registerProtocol(self, p):
pass
def unregisterProtocol(self, p):
pass
class PubSubFactory(MultiplexFactory):
broadcastFactory = BroadcastFactory()
def subscribe(self, p, name):
if name not in self._topics:
self._topics[name] = self.broadcastFactory
MultiplexFactory.subscribe(self, p, name)
class SockJSMultiplexResource(SockJSResource):
def __init__(self, options=None):
SockJSResource.__init__(self, MultiplexFactory(self), options)
def addFactory(self, name, factory):
return self._factory.addFactory(name, factory)
def broadcast(self, name, message):
return self._factory.broadcast(name, message)
def removeFactory(self, name):
return self._factory.removeFactory(name)
class SockJSPubSubResource(SockJSMultiplexResource):
def __init__(self, options=None):
SockJSResource.__init__(self, PubSubFactory(self), options)
# Copyright (c) 2012, Christopher Gamble
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the Christopher Gamble nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
from twisted.internet import reactor
# ============================================================================================================
# === THIS IS A MODIFIED COPY OF twisted.web.websockets TO BE COMPATIBLE WITH OLDER VERSIONS OF WEBSOCKETS ===
# === IT WILL BE REMOVED WHEN SOCKJS STOPS NEEDING TO SUPPORT OLD, DUMB VERSIONS OF WEBSOCKETS ===
# ============================================================================================================
# Copyright (c) 2011-2012 Oregon State University Open Source Lab
# 2011-2012 Corbin Simpson
# Twisted Matrix Laboratories
#
# See LICENSE for details.
"""
The WebSockets protocol (RFC 6455), provided as a resource which wraps a
factory.
"""
__all__ = ("OldWebSocketsResource",)
from base64 import b64encode, b64decode
from hashlib import md5, sha1
from struct import pack, unpack
from string import digits
from zope.interface import implementer
from twisted.protocols.policies import ProtocolWrapper, WrappingFactory
from twisted.python import log
from twisted.python.constants import NamedConstant, Names
from twisted.web.resource import IResource, NoResource
from twisted.web.server import NOT_DONE_YET
class _WSException(Exception):
"""
Internal exception for control flow inside the WebSockets frame parser.
"""
class _CONTROLS(Names):
"""
Control frame specifiers.
"""
NORMAL = NamedConstant()
CLOSE = NamedConstant()
PING = NamedConstant()
PONG = NamedConstant()
_opcode_types = {
0x0: _CONTROLS.NORMAL,
0x1: _CONTROLS.NORMAL,
0x2: _CONTROLS.NORMAL,
0x8: _CONTROLS.CLOSE,
0x9: _CONTROLS.PING,
0xa: _CONTROLS.PONG,
}
_opcode_for_type = {
_CONTROLS.NORMAL: 0x1,
_CONTROLS.CLOSE: 0x8,
_CONTROLS.PING: 0x9,
_CONTROLS.PONG: 0xa,
}
_encoders = {
"base64": b64encode,
}
_decoders = {
"base64": b64decode,
}
_WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
def _isHixie75(request):
return request.getHeader("Sec-WebSocket-Version") is None and \
request.getHeader("Sec-WebSocket-Key1") is None and \
request.getHeader("Sec-WebSocket-Key2") is None
def _isHybi00(request):
return request.getHeader("Sec-WebSocket-Key1") is not None and \
request.getHeader("Sec-WebSocket-Key2") is not None
def _challenge(key1, key2, challenge):
first = int("".join(i for i in key1 if i in digits)) / key1.count(" ")
second = int("".join(i for i in key2 if i in digits)) / key2.count(" ")
nonce = md5(pack(">II8s", first, second, challenge)).digest()
return nonce
def _makeAccept(key):
return sha1("%s%s" % (key, _WS_GUID)).digest().encode("base64").strip()
def _mask(buf, key):
key = [ord(i) for i in key]
buf = list(buf)
for i, char in enumerate(buf):
buf[i] = chr(ord(char) ^ key[i % 4])
return "".join(buf)
def _makeFrame(buf, old, _opcode=_CONTROLS.NORMAL):
if old:
if _opcode != _CONTROLS.NORMAL:
return None
return "\x00{}\xFF".format(buf)
else:
bufferLength = len(buf)
if bufferLength > 0xffff:
length = "\x7f%s" % pack(">Q", bufferLength)
elif bufferLength > 0x7d:
length = "\x7e%s" % pack(">H", bufferLength)
else:
length = chr(bufferLength)
# Always make a normal packet.
header = chr(0x80 | _opcode_for_type[_opcode])
frame = "%s%s%s" % (header, length, buf)
return frame
def _parseFrames(buf, old):
if old:
start = buf.find("\x00")
tail = 0
frames = []
while start != -1:
end = buf.find("\xFF",start+1)
if end == -1:
break
frame = buf[start+1:end]
frames.append((_CONTROLS.NORMAL, frame))
tail = end + 1
start = buf.find("\x00", tail)
return frames, buf[tail:]
else:
start = 0
frames = []
while True:
if len(buf) - start < 2:
break
header = ord(buf[start])
if header & 0x70:
raise _WSException("Reserved flag in frame (%d)" % header)
opcode = header & 0xf
try:
opcode = _opcode_types[opcode]
except KeyError:
raise _WSException("Unknown opcode %d in frame" % opcode)
length = ord(buf[start + 1])
masked = length & 0x80
length &= 0x7f
offset = 2
if length == 0x7e:
if len(buf) - start < 4:
break
length = buf[start + 2:start + 4]
length = unpack(">H", length)[0]
offset += 2
elif length == 0x7f:
if len(buf) - start < 10:
break
length = buf[start + 2:start + 10]
length = unpack(">Q", length)[0]
offset += 8
if masked:
if len(buf) - (start + offset) < 4:
break
key = buf[start + offset:start + offset + 4]
offset += 4
if len(buf) - (start + offset) < length:
break
data = buf[start + offset:start + offset + length]
if masked:
data = _mask(data, key)
if opcode == _CONTROLS.CLOSE:
if len(data) >= 2:
data = unpack(">H", data[:2])[0], data[2:]
else:
data = 1000, "No reason given"
frames.append((opcode, data))
start += offset + length
return frames, buf[start:]
class _WebSocketsProtocol(ProtocolWrapper):
buf = ""
codec = None
challenge = None
connected = False
pending_dc = False
def __init__(self, *args, **kwargs):
ProtocolWrapper.__init__(self, *args, **kwargs)
self._pending_frames = []
def connectionMade(self):
connected = True
if not self.challenge:
ProtocolWrapper.connectionMade(self)
log.msg("Opening connection with %s" % self.transport.getPeer())
def parseFrames(self):
try:
frames, self.buf = _parseFrames(self.buf, self.old)
except _WSException:
log.err()
self.loseConnection()
return
for frame in frames:
opcode, data = frame
if opcode == _CONTROLS.NORMAL:
if self.codec:
data = _decoders[self.codec](data)
ProtocolWrapper.dataReceived(self, data)
elif opcode == _CONTROLS.CLOSE:
reason, text = data
log.msg("Closing connection: %r (%d)" % (text, reason))
self.transport.loseConnection()
return
elif opcode == _CONTROLS.PING:
self.transport.write(_makeFrame(data, self.old, _opcode=_CONTROLS.PONG))
def sendFrames(self):
# Don't send anything before the challenge
if self.challenge:
return
for frame in self._pending_frames:
# Encode the frame before sending it.
if self.codec:
frame = _encoders[self.codec](frame)
packet = _makeFrame(frame, self.old)
self.transport.write(packet)
self._pending_frames = []
def dataReceived(self, data):
self.buf += data
if self.challenge:
if len(self.buf) >= 8:
challenge, self.buf = self.buf[:8], self.buf[8:]
nonce = self.challenge(challenge)
self.transport.write(nonce)
self.challenge = None
if self.connected:
ProtocolWrapper.connectionMade(self)
self.dataReceived("") # Kick it off proper
if self.pending_dc:
self.pending_dc = False
self.loseConnection()
else:
self.parseFrames()
if self._pending_frames:
self.sendFrames()
def write(self, data):
self._pending_frames.append(data)
self.sendFrames()
def writeSequence(self, data):
self._pending_frames.extend(data)
self.sendFrames()
def loseConnection(self):
if not self.disconnecting:
if not self.challenge:
self.disconnecting = True
frame = _makeFrame("", self.old, _opcode=_CONTROLS.CLOSE)
if frame:
self.transport.write(frame)
else:
self.transport.loseConnection()
else:
self.pending_dc = True
class _WebSocketsFactory(WrappingFactory):
protocol = _WebSocketsProtocol
@implementer(IResource)
class OldWebSocketsResource(object):
isLeaf = True
def __init__(self, factory):
self.__factory = _WebSocketsFactory(factory)
def getChildWithDefault(self, name, request):
return NoResource("No such child resource.")
def putChild(self, path, child):
pass
def render(self, request):
"""
Render a request.
We're not actually rendering a request. We are secretly going to
handle a WebSockets connection instead.
"""
# If we fail at all, we're gonna fail with 400 and no response.
failed = False
if request.method != "GET":
failed = True
upgrade = request.getHeader("Upgrade")
if upgrade is None or "websocket" not in upgrade.lower():
failed = True
connection = request.getHeader("Connection")
if connection is None or "upgrade" not in connection.lower():
failed = True
codec = request.getHeader("Sec-WebSocket-Protocol") or request.getHeader("WebSocket-Protocol")
if codec:
if codec not in _encoders or codec not in _decoders:
log.msg("Codec %s is not implemented" % codec)
failed = True
## This is a big mess of setting various headers based on which version we are
## And determining whether to use "old frames" or "new frames"
if _isHixie75(request) or _isHybi00(request):
old = True
host = request.getHeader("Host") or "example.com"
origin = request.getHeader("Origin") or "http://example.com"
location = "{}://{}{}".format("wss" if request.isSecure() else "ws", host, request.path)
if _isHixie75(request):
request.setHeader("WebSocket-Origin", origin)
request.setHeader("WebSocket-Location", location)
if codec:
request.setHeader("WebSocket-Protocol", codec)
else:
request.setHeader("Sec-WebSocket-Origin", origin)
request.setHeader("Sec-WebSocket-Location", location)
if codec:
request.setHeader("Sec-WebSocket-Protocol", codec)
else:
old = False
key = request.getHeader("Sec-WebSocket-Key")
if key is None:
failed = True
version = request.getHeader("Sec-WebSocket-Version")
if version not in ("7","8","13"):
failed = True
request.setHeader("Sec-WebSocket-Version", "13")
if not failed:
request.setHeader("Sec-WebSocket-Version", version)
request.setHeader("Sec-WebSocket-Accept", _makeAccept(key))
if codec:
request.setHeader("Sec-WebSocket-Protocol", codec)
if failed:
request.setResponseCode(400)
return ""
# We are going to finish this handshake. We will return a valid status code.
request.setResponseCode(101)
request.setHeader("Upgrade", "WebSocket")
request.setHeader("Connection", "Upgrade")
# Create the protocol. This could fail, in which case we deliver an
# error status. Status 502 was decreed by glyph; blame him.
protocol = self.__factory.buildProtocol(request.transport.getPeer())
if not protocol:
request.setResponseCode(502)
return ""
protocol.old = old
if _isHybi00(request):
protocol.challenge = lambda x: _challenge(request.getHeader("Sec-WebSocket-Key1"), request.getHeader("Sec-WebSocket-Key2"), x)
if codec:
protocol.codec = codec
## This must be first, since makeConnection will butcher our headers otherwise
request.write("")
## Then we wire it into the protocol wrapper
transport, request.transport = request.transport, None
transport.protocol = protocol
protocol.makeConnection(transport)
## Copy the buffer
protocol.dataReceived(request.channel.clearLineBuffer())
return NOT_DONE_YET
# Copyright (c) 2012, Christopher Gamble
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the Christopher Gamble nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
from zope.interface import directlyProvides, providedBy
from twisted.internet import reactor, protocol
from twisted.web import resource, server, http
from twisted.protocols.policies import ProtocolWrapper
from txsockjs.utils import normalize
import json
class StubResource(resource.Resource, ProtocolWrapper):
isLeaf = True
def __init__(self, parent, session):
resource.Resource.__init__(self)
ProtocolWrapper.__init__(self, None, session)
self.parent = parent
self.session = session
self.putChild("", self)
def render_OPTIONS(self, request):
method = "POST" if getattr(self, "render_POST", None) is not None else "GET"
request.setResponseCode(http.NO_CONTENT)
self.parent.setBaseHeaders(request,False)
request.setHeader('Cache-Control', 'public, max-age=31536000')
request.setHeader('access-control-max-age', '31536000')
request.setHeader('Expires', 'Fri, 01 Jan 2500 00:00:00 GMT') #Get a new library by then
request.setHeader('Access-Control-Allow-Methods', 'OPTIONS, {}'.format(method)) # Hardcoding this may be bad?
return ""
def connect(self, request):
if self.session.attached:
return 'c[2010,"Another connection still open"]\n'
self.request = request
directlyProvides(self, providedBy(request.transport))
protocol.Protocol.makeConnection(self, request.transport)
self.session.makeConnection(self)
request.notifyFinish().addErrback(self.connectionLost)
return server.NOT_DONE_YET
def disconnect(self):
self.request.finish()
self.session.transportLeft()
def loseConnection(self):
self.request.finish()
self.session.transportLeft()
def connectionLost(self, reason=None):
self.wrappedProtocol.connectionLost(reason)
class Stub(ProtocolWrapper):
def __init__(self, parent, session):
self.parent = parent
self.session = session
self.pending = []
self.buffer = []
self.connecting = True
self.disconnecting = False
self.attached = False
self.transport = None # Upstream (SockJS)
self.protocol = None # Downstream (Wrapped Factory)
self.peer = None
self.host = None
self.timeout = reactor.callLater(self.parent._options['timeout'], self.disconnect)
#reactor.callLater(self.parent._options['heartbeat'], self.heartbeat)
def makeConnection(self, transport):
directlyProvides(self, providedBy(transport))
protocol.Protocol.makeConnection(self, transport)
self.attached = True
self.peer = self.transport.getPeer()
self.host = self.transport.getHost()
if self.timeout.active():
self.timeout.cancel()
if self.protocol is None:
self.protocol = self.parent._factory.buildProtocol(self.transport.getPeer())
self.protocol.makeConnection(self)
self.sendData()
def loseConnection(self):
self.disconnecting = True
self.sendData()
def connectionLost(self, reason=None):
if self.attached:
self.disconnecting = True
self.transport = None
self.attached = False
self.disconnect()
def heartbeat(self):
self.pending.append('h')
reactor.callLater(self.parent._options['heartbeat'], self.heartbeat)
def disconnect(self):
if self.protocol:
self.protocol.connectionLost(None)
del self.parent._sessions[self.session]
def transportLeft(self):
self.transport = None
self.attached = False
self.timeout = reactor.callLater(self.parent._options['timeout'], self.disconnect)
def write(self, data):
data = normalize(data, self.parent._options['encoding'])
self.buffer.append(data)
self.sendData()
def writeSequence(self, data):
for p in data:
p = normalize(p, self.parent._options['encoding'])
self.buffer.extend(data)
self.sendData()
def writeRaw(self, data):
self.flushData()
self.pending.append(data)
self.sendData()
def sendData(self):
if self.transport:
if self.connecting:
self.transport.write('o')
self.connecting = False
self.sendData()
elif self.disconnecting:
self.transport.write('c[3000,"Go away!"]')
if self.transport:
self.transport.loseConnection()
else:
self.flushData()
if self.pending:
data = list(self.pending)
self.pending = []
self.transport.writeSequence(data)
def flushData(self):
if self.buffer:
data = 'a{}'.format(json.dumps(self.buffer, separators=(',',':')))
self.buffer = []
self.pending.append(data)
def requeue(self, data):
data.extend(self.pending)
self.pending = data
def dataReceived(self, data):
if self.timeout.active():
self.timeout.reset(5)
if data == '':
return "Payload expected."
try:
packets = json.loads(data)
for p in packets:
p = normalize(p, self.parent._options['encoding'])
if self.protocol:
self.protocol.dataReceived(p)
return None
except ValueError:
return "Broken JSON encoding."
def getPeer(self):
return self.peer
def getHost(self):
return self.host
def registerProducer(self, producer, streaming):
if self.transport:
self.transport.registerProducer(producer, streaming)
def unregisterProducer(self):
if self.transport:
self.transport.unregisterProducer()
def stopConsuming(self):
if self.transport:
self.transport.stopConsuming()
# Copyright (c) 2012, Christopher Gamble
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the Christopher Gamble nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
from txsockjs.protocols.base import StubResource
class EventSource(StubResource):
sent = 0
done = False
def render_GET(self, request):
self.parent.setBaseHeaders(request)
request.setHeader('content-type', 'text/event-stream; charset=UTF-8')
request.write("\r\n")
return self.connect(request)
def write(self, data):
if self.done:
self.session.requeue([data])
return
packet = "data: {}\r\n\r\n".format(data)
self.sent += len(packet)
self.request.write(packet)
if self.sent > self.parent._options['streaming_limit']:
self.done = True
self.disconnect()
def writeSequence(self, data):
for d in data:
self.write(d)
# Copyright (c) 2012, Christopher Gamble
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the Christopher Gamble nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
from twisted.web import http
from txsockjs.protocols.base import StubResource
class HTMLFile(StubResource):
sent = 0
done = False
def render_GET(self, request):
self.parent.setBaseHeaders(request)
callback = request.args.get('c',[None])[0]
if callback is None:
request.setResponseCode(http.INTERNAL_SERVER_ERROR)
return '"callback" parameter required'
request.setHeader('content-type', 'text/html; charset=UTF-8')
request.write(r'''
<!doctype html>
<html><head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head><body><h2>Don't panic!</h2>
<script>
document.domain = document.domain;
var c = parent.{};
c.start();
function p(d) {{c.message(d);}};
window.onload = function() {{c.stop();}};
</script>{}
'''.format(callback, ' '*1024))
return self.connect(request)
def write(self, data):
if self.done:
self.session.requeue([data])
return
packet = "<script>\np(\"{}\");\n</script>\r\n".format(data.replace('\\','\\\\').replace('"','\\"'))
self.sent += len(packet)
self.request.write(packet)
if self.sent > self.parent._options['streaming_limit']:
self.done = True
self.disconnect()
def writeSequence(self, data):
for d in data:
self.write(d)
# Copyright (c) 2012, Christopher Gamble
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the Christopher Gamble nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
from twisted.web import http
from txsockjs.protocols.base import StubResource
class JSONP(StubResource):
written = False
def render_GET(self, request):
self.parent.setBaseHeaders(request)
self.callback = request.args.get('c',[None])[0]
if self.callback is None:
request.setResponseCode(http.INTERNAL_SERVER_ERROR)
return '"callback" parameter required'
request.setHeader('content-type', 'application/javascript; charset=UTF-8')
return self.connect(request)
def write(self, data):
if self.written:
self.session.requeue([data])
return
self.written = True
self.request.write("{}(\"{}\");\r\n".format(self.callback, data.replace('\\','\\\\').replace('"','\\"')))
self.disconnect()
def writeSequence(self, data):
self.write(data.pop(0))
self.session.requeue(data)
class JSONPSend(StubResource):
def render_POST(self, request):
self.parent.setBaseHeaders(request)
request.setHeader('content-type', 'text/plain; charset=UTF-8')
urlencoded = request.getHeader("Content-Type") == 'application/x-www-form-urlencoded'
data = request.args.get('d', [''])[0] if urlencoded else request.content.read()
ret = self.session.dataReceived(data)
if not ret:
return "ok"
request.setResponseCode(http.INTERNAL_SERVER_ERROR)
return "{}\r\n".format(ret)
# Copyright (c) 2012, Christopher Gamble
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the Christopher Gamble nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
import json, random
from twisted.web import resource, http
class Info(resource.Resource):
def render_GET(self, request):
self.parent.setBaseHeaders(request,False)
request.setHeader('content-type', 'application/json; charset=UTF-8')
data = {
'websocket': self.parent._options['websocket'],
'cookie_needed': self.parent._options['cookie_needed'],
'origins': ['*:*'],
'entropy': random.randint(0,2**32-1)
}
return json.dumps(data)
def render_OPTIONS(self, request):
request.setResponseCode(http.NO_CONTENT)
self.parent.setBaseHeaders(request,False)
request.setHeader('Cache-Control', 'public, max-age=31536000')
request.setHeader('access-control-max-age', '31536000')
request.setHeader('Expires', 'Fri, 01 Jan 2500 00:00:00 GMT') #Get a new library by then
request.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET') # Hardcoding this may be bad?
return ""
class IFrame(resource.Resource):
etag = '00000000-0000-0000-0000-000000000000'
def render_GET(self, request):
self.parent.setBaseHeaders(request,False)
if request.setETag(self.etag):
request.setResponseCode(http.NOT_MODIFIED)
return ""
request.setHeader('content-type', 'text/html; charset=UTF-8')
request.setHeader('Cache-Control', 'public, max-age=31536000')
request.setHeader('access-control-max-age', '31536000')
request.setHeader('Expires', 'Fri, 01 Jan 2500 00:00:00 GMT') #Get a new library by then
return '''
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script>
document.domain = document.domain;
_sockjs_onload = function(){{SockJS.bootstrap_iframe();}};
</script>
<script src="{}"></script>
</head>
<body>
<h2>Don't panic!</h2>
<p>This is a SockJS hidden iframe. It's used for cross domain magic.</p>
</body>
</html>'''.format(self.parent._options["sockjs_url"])
# Copyright (c) 2012, Christopher Gamble
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the Christopher Gamble nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
try:
from twisted.web.websockets import WebSocketsResource
except ImportError:
from txsockjs.websockets import WebSocketsResource
from zope.interface import directlyProvides, providedBy
from twisted.internet.protocol import Protocol
from twisted.protocols.policies import WrappingFactory, ProtocolWrapper
from twisted.web.server import NOT_DONE_YET
from txsockjs.oldwebsockets import OldWebSocketsResource
from txsockjs.utils import normalize
import json
class JsonProtocol(ProtocolWrapper):
def makeConnection(self, transport):
directlyProvides(self, providedBy(transport))
Protocol.makeConnection(self, transport)
self.transport.write("o")
self.factory.registerProtocol(self)
self.wrappedProtocol.makeConnection(self)
def write(self, data):
self.writeSequence([data])
def writeSequence(self, data):
for p in data:
p = normalize(p, self.factory.parent._options['encoding'])
self.transport.write("a{}".format(json.dumps(data, separators=(',',':'))))
def writeRaw(self, data):
self.transport.write(data)
def loseConnection(self):
self.transport.write('c[3000,"Go away!"]')
ProtocolWrapper.loseConnection(self)
def dataReceived(self, data):
if not data:
return
try:
dat = json.loads(data)
except ValueError:
self.transport.loseConnection()
else:
for d in dat:
ProtocolWrapper.dataReceived(self, d)
class JsonFactory(WrappingFactory):
protocol = JsonProtocol
class RawWebSocket(WebSocketsResource, OldWebSocketsResource):
def __init__(self):
self._factory = None
def _makeFactory(self):
WebSocketsResource.__init__(self, self.parent._factory)
OldWebSocketsResource.__init__(self, self.parent._factory)
def render(self, request):
# Get around .parent limitation
if self._factory is None:
self._makeFactory()
# Override handling of invalid methods, returning 400 makes SockJS mad
if request.method != 'GET':
request.setResponseCode(405)
request.defaultContentType = None # SockJS wants this gone
request.setHeader('Allow','GET')
return ""
# Override handling of lack of headers, again SockJS requires non-RFC stuff
upgrade = request.getHeader("Upgrade")
if upgrade is None or "websocket" not in upgrade.lower():
request.setResponseCode(400)
return 'Can "Upgrade" only to "WebSocket".'
connection = request.getHeader("Connection")
if connection is None or "upgrade" not in connection.lower():
request.setResponseCode(400)
return '"Connection" must be "Upgrade".'
# Defer to inherited methods
ret = WebSocketsResource.render(self, request) # For RFC versions of websockets
if ret is NOT_DONE_YET:
return ret
return OldWebSocketsResource.render(self, request) # For non-RFC versions of websockets
class WebSocket(RawWebSocket):
def _makeFactory(self):
f = JsonFactory(self.parent._factory)
f.parent = self.parent
WebSocketsResource.__init__(self, f)
OldWebSocketsResource.__init__(self, f)
# Copyright (c) 2012, Christopher Gamble
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the Christopher Gamble nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
from twisted.web import resource, http
from txsockjs.protocols.base import StubResource
class XHR(StubResource):
written = False
def render_POST(self, request):
self.parent.setBaseHeaders(request)
request.setHeader('content-type', 'application/javascript; charset=UTF-8')
return self.connect(request)
def write(self, data):
if self.written:
self.session.requeue([data])
return
self.written = True
self.request.write("{}\n".format(data))
self.disconnect()
def writeSequence(self, data):
if not self.written:
self.write(data.pop(0))
self.session.requeue(data)
class XHRSend(StubResource):
def render_POST(self, request):
self.parent.setBaseHeaders(request)
request.setResponseCode(http.NO_CONTENT)
request.setHeader('content-type', 'text/plain; charset=UTF-8')
ret = self.session.dataReceived(request.content.read())
if not ret:
return ""
request.setResponseCode(http.INTERNAL_SERVER_ERROR)
return "{}\r\n".format(ret)
class XHRStream(StubResource):
sent = 0
done = False
def render_POST(self, request):
self.parent.setBaseHeaders(request)
request.setHeader('content-type', 'application/javascript; charset=UTF-8')
request.write("{}\n".format('h'*2048))
return self.connect(request)
def write(self, data):
if self.done:
self.session.requeue([data])
return
packet = "{}\n".format(data)
self.sent += len(packet)
self.request.write(packet)
if self.sent > self.parent._options['streaming_limit']:
self.done = True
self.disconnect()
def writeSequence(self, data):
for d in data:
self.write(d)
# Copyright (c) 2012, Christopher Gamble
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the Christopher Gamble nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
def normalize(s, encoding):
if not isinstance(s, str):
try:
return str(s)
except UnicodeEncodeError:
return str(s).encode('utf-8','backslashreplace')
elif isinstance(s, str):
return s.encode('utf-8', 'backslashreplace')
else:
if s.decode('utf-8', 'ignore').encode('utf-8', 'ignore') == s: # Ensure s is a valid UTF-8 string
return s
else: # Otherwise assume it is Windows 1252
return s.decode(encoding, 'replace').encode('utf-8', 'backslashreplace')
def broadcast(message, targets, encoding="cp1252"):
message = normalize(message, encoding)
message = 'a{}'.format(json.dumps([message], separators=(',',':')))
for t in targets:
t.writeRaw(message)
\ No newline at end of file
# Copyright (c) 2012, Christopher Gamble
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the Christopher Gamble nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
def normalize(s, encoding):
if not isinstance(s, basestring):
try:
return str(s)
except UnicodeEncodeError:
return unicode(s).encode('utf-8','backslashreplace')
elif isinstance(s, unicode):
return s.encode('utf-8', 'backslashreplace')
else:
if s.decode('utf-8', 'ignore').encode('utf-8', 'ignore') == s: # Ensure s is a valid UTF-8 string
return s
else: # Otherwise assume it is Windows 1252
return s.decode(encoding, 'replace').encode('utf-8', 'backslashreplace')
def broadcast(message, targets, encoding="cp1252"):
message = normalize(message, encoding)
message = 'a{}'.format(json.dumps([message], separators=(',',':')))
for t in targets:
t.writeRaw(message)
\ No newline at end of file
# =====================================================================================
# === THIS IS A DIRECT COPY OF twisted.web.websockets AS IT IS STILL IN DEVELOPMENT ===
# === IT WILL BE REPLACED BY THE ACTUAL VERSION WHEN IT IS PUBLICLY AVAILABLE. ===
# =====================================================================================
# Copyright (c) 2011-2012 Oregon State University Open Source Lab
# 2011-2012 Corbin Simpson
# Twisted Matrix Laboratories
#
# See LICENSE for details.
"""
The WebSockets protocol (RFC 6455), provided as a resource which wraps a
factory.
"""
__all__ = ("WebSocketsResource",)
from base64 import b64encode, b64decode
from hashlib import sha1
from struct import pack, unpack
from zope.interface import implementer
from twisted.protocols.policies import ProtocolWrapper, WrappingFactory
from twisted.python import log
from twisted.python.constants import NamedConstant, Names
from twisted.web.resource import IResource, NoResource
from twisted.web.server import NOT_DONE_YET
class _WSException(Exception):
"""
Internal exception for control flow inside the WebSockets frame parser.
"""
# Control frame specifiers. Some versions of WS have control signals sent
# in-band. Adorable, right?
class _CONTROLS(Names):
"""
Control frame specifiers.
"""
NORMAL = NamedConstant()
CLOSE = NamedConstant()
PING = NamedConstant()
PONG = NamedConstant()
_opcode_types = {
0x0: _CONTROLS.NORMAL,
0x1: _CONTROLS.NORMAL,
0x2: _CONTROLS.NORMAL,
0x8: _CONTROLS.CLOSE,
0x9: _CONTROLS.PING,
0xa: _CONTROLS.PONG,
}
_opcode_for_type = {
_CONTROLS.NORMAL: 0x1,
_CONTROLS.CLOSE: 0x8,
_CONTROLS.PING: 0x9,
_CONTROLS.PONG: 0xa,
}
_encoders = {
"base64": b64encode,
}
_decoders = {
"base64": b64decode,
}
# Authentication for WS.
# The GUID for WebSockets, from RFC 6455.
_WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
def _makeAccept(key):
"""
Create an "accept" response for a given key.
This dance is expected to somehow magically make WebSockets secure.
@type key: C{str}
@param key: The key to respond to.
@rtype: C{str}
@return: An encoded response.
"""
return sha1("%s%s" % (key, _WS_GUID)).digest().encode("base64").strip()
# Frame helpers.
# Separated out to make unit testing a lot easier.
# Frames are bonghits in newer WS versions, so helpers are appreciated.
def _mask(buf, key):
"""
Mask or unmask a buffer of bytes with a masking key.
@type buf: C{str}
@param buf: A buffer of bytes.
@type key: C{str}
@param key: The masking key. Must be exactly four bytes.
@rtype: C{str}
@return: A masked buffer of bytes.
"""
# This is super-secure, I promise~
key = [ord(i) for i in key]
buf = list(buf)
for i, char in enumerate(buf):
buf[i] = chr(ord(char) ^ key[i % 4])
return "".join(buf)
def _makeFrame(buf, _opcode=_CONTROLS.NORMAL):
"""
Make a frame.
This function always creates unmasked frames, and attempts to use the
smallest possible lengths.
@type buf: C{str}
@param buf: A buffer of bytes.
@type _opcode: C{_CONTROLS}
@param _opcode: Which type of frame to create.
@rtype: C{str}
@return: A packed frame.
"""
bufferLength = len(buf)
if bufferLength > 0xffff:
length = "\x7f%s" % pack(">Q", bufferLength)
elif bufferLength > 0x7d:
length = "\x7e%s" % pack(">H", bufferLength)
else:
length = chr(bufferLength)
# Always make a normal packet.
header = chr(0x80 | _opcode_for_type[_opcode])
frame = "%s%s%s" % (header, length, buf)
return frame
def _parseFrames(buf):
"""
Parse frames in a highly compliant manner.
@type buf: C{str}
@param buf: A buffer of bytes.
@rtype: C{list}
@return: A list of frames.
"""
start = 0
frames = []
while True:
# If there's not at least two bytes in the buffer, bail.
if len(buf) - start < 2:
break
# Grab the header. This single byte holds some flags nobody cares
# about, and an opcode which nobody cares about.
header = ord(buf[start])
if header & 0x70:
# At least one of the reserved flags is set. Pork chop sandwiches!
raise _WSException("Reserved flag in frame (%d)" % header)
# Get the opcode, and translate it to a local enum which we actually
# care about.
opcode = header & 0xf
try:
opcode = _opcode_types[opcode]
except KeyError:
raise _WSException("Unknown opcode %d in frame" % opcode)
# Get the payload length and determine whether we need to look for an
# extra length.
length = ord(buf[start + 1])
masked = length & 0x80
length &= 0x7f
# The offset we're gonna be using to walk through the frame. We use
# this because the offset is variable depending on the length and
# mask.
offset = 2
# Extra length fields.
if length == 0x7e:
if len(buf) - start < 4:
break
length = buf[start + 2:start + 4]
length = unpack(">H", length)[0]
offset += 2
elif length == 0x7f:
if len(buf) - start < 10:
break
# Protocol bug: The top bit of this long long *must* be cleared;
# that is, it is expected to be interpreted as signed. That's
# fucking stupid, if you don't mind me saying so, and so we're
# interpreting it as unsigned anyway. If you wanna send exabytes
# of data down the wire, then go ahead!
length = buf[start + 2:start + 10]
length = unpack(">Q", length)[0]
offset += 8
if masked:
if len(buf) - (start + offset) < 4:
# This is not strictly necessary, but it's more explicit so
# that we don't create an invalid key.
break
key = buf[start + offset:start + offset + 4]
offset += 4
if len(buf) - (start + offset) < length:
break
data = buf[start + offset:start + offset + length]
if masked:
data = _mask(data, key)
if opcode == _CONTROLS.CLOSE:
if len(data) >= 2:
# Gotta unpack the opcode and return usable data here.
data = unpack(">H", data[:2])[0], data[2:]
else:
# No reason given; use generic data.
data = 1000, "No reason given"
frames.append((opcode, data))
start += offset + length
return frames, buf[start:]
class _WebSocketsProtocol(ProtocolWrapper):
"""
Protocol which wraps another protocol to provide a WebSockets transport
layer.
"""
buf = ""
codec = None
def __init__(self, *args, **kwargs):
ProtocolWrapper.__init__(self, *args, **kwargs)
self._pending_frames = []
def connectionMade(self):
ProtocolWrapper.connectionMade(self)
log.msg("Opening connection with %s" % self.transport.getPeer())
def parseFrames(self):
"""
Find frames in incoming data and pass them to the underlying protocol.
"""
try:
frames, self.buf = _parseFrames(self.buf)
except _WSException:
# Couldn't parse all the frames, something went wrong, let's bail.
log.err()
self.loseConnection()
return
for frame in frames:
opcode, data = frame
if opcode == _CONTROLS.NORMAL:
# Business as usual. Decode the frame, if we have a decoder.
if self.codec:
data = _decoders[self.codec](data)
# Pass the frame to the underlying protocol.
ProtocolWrapper.dataReceived(self, data)
elif opcode == _CONTROLS.CLOSE:
# The other side wants us to close. I wonder why?
reason, text = data
log.msg("Closing connection: %r (%d)" % (text, reason))
# Close the connection.
self.loseConnection()
return
elif opcode == _CONTROLS.PING:
# 5.5.2 PINGs must be responded to with PONGs.
# 5.5.3 PONGs must contain the data that was sent with the
# provoking PING.
self.transport.write(_makeFrame(data, _opcode=_CONTROLS.PONG))
def sendFrames(self):
"""
Send all pending frames.
"""
for frame in self._pending_frames:
# Encode the frame before sending it.
if self.codec:
frame = _encoders[self.codec](frame)
packet = _makeFrame(frame)
self.transport.write(packet)
self._pending_frames = []
def dataReceived(self, data):
self.buf += data
self.parseFrames()
# Kick any pending frames. This is needed because frames might have
# started piling up early; we can get write()s from our protocol above
# when they makeConnection() immediately, before our browser client
# actually sends any data. In those cases, we need to manually kick
# pending frames.
if self._pending_frames:
self.sendFrames()
def write(self, data):
"""
Write to the transport.
This method will only be called by the underlying protocol.
"""
self._pending_frames.append(data)
self.sendFrames()
def writeSequence(self, data):
"""
Write a sequence of data to the transport.
This method will only be called by the underlying protocol.
"""
self._pending_frames.extend(data)
self.sendFrames()
def loseConnection(self):
"""
Close the connection.
This includes telling the other side we're closing the connection.
If the other side didn't signal that the connection is being closed,
then we might not see their last message, but since their last message
should, according to the spec, be a simple acknowledgement, it
shouldn't be a problem.
"""
# Send a closing frame. It's only polite. (And might keep the browser
# from hanging.)
if not self.disconnecting:
frame = _makeFrame("", _opcode=_CONTROLS.CLOSE)
self.transport.write(frame)
ProtocolWrapper.loseConnection(self)
class _WebSocketsFactory(WrappingFactory):
"""
Factory which wraps another factory to provide WebSockets frames for all
of its protocols.
This factory does not provide the HTTP headers required to perform a
WebSockets handshake; see C{WebSocketsResource}.
"""
protocol = _WebSocketsProtocol
@implementer(IResource)
class WebSocketsResource(object):
"""
A resource for serving a protocol through WebSockets.
This class wraps a factory and connects it to WebSockets clients. Each
connecting client will be connected to a new protocol of the factory.
Due to unresolved questions of logistics, this resource cannot have
children.
@since 12.3
"""
isLeaf = True
def __init__(self, factory):
self._factory = _WebSocketsFactory(factory)
def getChildWithDefault(self, name, request):
return NoResource("No such child resource.")
def putChild(self, path, child):
pass
def render(self, request):
"""
Render a request.
We're not actually rendering a request. We are secretly going to
handle a WebSockets connection instead.
"""
# If we fail at all, we're gonna fail with 400 and no response.
# You might want to pop open the RFC and read along.
failed = False
if request.method != "GET":
# 4.2.1.1 GET is required.
failed = True
upgrade = request.getHeader("Upgrade")
if upgrade is None or "websocket" not in upgrade.lower():
# 4.2.1.3 Upgrade: WebSocket is required.
failed = True
connection = request.getHeader("Connection")
if connection is None or "upgrade" not in connection.lower():
# 4.2.1.4 Connection: Upgrade is required.
failed = True
key = request.getHeader("Sec-WebSocket-Key")
if key is None:
# 4.2.1.5 The challenge key is required.
failed = True
version = request.getHeader("Sec-WebSocket-Version")
if version != "13":
# 4.2.1.6 Only version 13 works.
failed = True
# 4.4 Forward-compatible version checking.
request.setHeader("Sec-WebSocket-Version", "13")
# Check whether a codec is needed. WS calls this a "protocol" for
# reasons I cannot fathom. The specification permits multiple,
# comma-separated codecs to be listed, but this functionality isn't
# used in the wild. (If that ever changes, we'll have already added
# the requisite codecs here anyway.) The main reason why we check for
# codecs at all is that older draft versions of WebSockets used base64
# encoding to work around the inability to send \x00 bytes, and those
# runtimes would request base64 encoding during the handshake. We
# stand prepared to engage that behavior should any of those runtimes
# start supporting RFC WebSockets.
#
# We probably should remove this altogether, but I'd rather leave it
# because it will prove to be a useful reference if/when extensions
# are added, and it *does* work as advertised.
codec = request.getHeader("Sec-WebSocket-Protocol")
if codec:
if codec not in _encoders or codec not in _decoders:
log.msg("Codec %s is not implemented" % codec)
failed = True
if failed:
request.setResponseCode(400)
return ""
# We are going to finish this handshake. We will return a valid status
# code.
# 4.2.2.5.1 101 Switching Protocols
request.setResponseCode(101)
# 4.2.2.5.2 Upgrade: websocket
request.setHeader("Upgrade", "WebSocket")
# 4.2.2.5.3 Connection: Upgrade
request.setHeader("Connection", "Upgrade")
# 4.2.2.5.4 Response to the key challenge
request.setHeader("Sec-WebSocket-Accept", _makeAccept(key))
# 4.2.2.5.5 Optional codec declaration
if codec:
request.setHeader("Sec-WebSocket-Protocol", codec)
# Create the protocol. This could fail, in which case we deliver an
# error status. Status 502 was decreed by glyph; blame him.
protocol = self._factory.buildProtocol(request.transport.getPeer())
if not protocol:
request.setResponseCode(502)
return ""
if codec:
protocol.codec = codec
# Provoke request into flushing headers and finishing the handshake.
request.write("")
# And now take matters into our own hands. We shall manage the
# transport's lifecycle.
transport, request.transport = request.transport, None
# Connect the transport to our factory, and make things go. We need to
# do some stupid stuff here; see #3204, which could fix it.
transport.protocol = protocol
protocol.makeConnection(transport)
return NOT_DONE_YET
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment