243 lines
8.9 KiB
Python
243 lines
8.9 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
#!/usr/bin/env python
|
||
|
|
||
|
"""
|
||
|
This file is part of the web2py Web Framework
|
||
|
Copyrighted by Massimo Di Pierro <mdip...@cs.depaul.edu>
|
||
|
License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
|
||
|
|
||
|
Attention: Requires Chrome or Safari. For IE of Firefox you need https://github.com/gimite/web-socket-js
|
||
|
|
||
|
1) install tornado (requires Tornado 3.0 or later)
|
||
|
|
||
|
easy_install tornado
|
||
|
|
||
|
2) start this app:
|
||
|
|
||
|
python gluon/contrib/websocket_messaging.py -k mykey -p 8888
|
||
|
|
||
|
3) from any web2py app you can post messages with
|
||
|
|
||
|
from gluon.contrib.websocket_messaging import websocket_send
|
||
|
websocket_send('http://127.0.0.1:8888', 'Hello World', 'mykey', 'mygroup')
|
||
|
|
||
|
4) from any template you can receive them with
|
||
|
|
||
|
<script>
|
||
|
$(document).ready(function(){
|
||
|
if(!$.web2py.web2py_websocket('ws://127.0.0.1:8888/realtime/mygroup', function(e){alert(e.data)}))
|
||
|
|
||
|
alert("html5 websocket not supported by your browser, try Google Chrome");
|
||
|
});
|
||
|
</script>
|
||
|
|
||
|
When the server posts a message, all clients connected to the page will popup an alert message
|
||
|
Or if you want to send json messages and store evaluated json in a var called data:
|
||
|
|
||
|
<script>
|
||
|
$(document).ready(function(){
|
||
|
var data;
|
||
|
$.web2py.web2py_websocket('ws://127.0.0.1:8888/realtime/mygroup', function(e){data=eval('('+e.data+')')});
|
||
|
});
|
||
|
</script>
|
||
|
|
||
|
- All communications between web2py and websocket_messaging will be digitally signed with hmac.
|
||
|
- All validation is handled on the web2py side and there is no need to modify websocket_messaging.py
|
||
|
- Multiple web2py instances can talk with one or more websocket_messaging servers.
|
||
|
- "ws://127.0.0.1:8888/realtime/" must be contain the IP of the websocket_messaging server.
|
||
|
- Via group='mygroup' name you can support multiple groups of clients (think of many chat-rooms)
|
||
|
|
||
|
|
||
|
Here is a complete sample web2py action:
|
||
|
|
||
|
def index():
|
||
|
form=LOAD('default', 'ajax_form', ajax=True)
|
||
|
script=SCRIPT('''
|
||
|
jQuery(document).ready(function(){
|
||
|
var callback=function(e){alert(e.data)};
|
||
|
if(!$.web2py.web2py_websocket('ws://127.0.0.1:8888/realtime/mygroup', callback))
|
||
|
|
||
|
alert("html5 websocket not supported by your browser, try Google Chrome");
|
||
|
});
|
||
|
''')
|
||
|
return dict(form=form, script=script)
|
||
|
|
||
|
def ajax_form():
|
||
|
form=SQLFORM.factory(Field('message'))
|
||
|
if form.accepts(request,session):
|
||
|
from gluon.contrib.websocket_messaging import websocket_send
|
||
|
websocket_send(
|
||
|
'http://127.0.0.1:8888', form.vars.message, 'mykey', 'mygroup')
|
||
|
return form
|
||
|
|
||
|
https is possible too using 'https://127.0.0.1:8888' instead of 'http://127.0.0.1:8888', but need to
|
||
|
be started with
|
||
|
|
||
|
python gluon/contrib/websocket_messaging.py -k mykey -p 8888 -s keyfile.pem -c certfile.pem
|
||
|
|
||
|
for secure websocket do:
|
||
|
|
||
|
web2py_websocket('wss://127.0.0.1:8888/realtime/mygroup',callback)
|
||
|
|
||
|
Acknowledgements:
|
||
|
Tornado code inspired by http://thomas.pelletier.im/2010/08/websocket-tornado-redis/
|
||
|
|
||
|
"""
|
||
|
from __future__ import print_function
|
||
|
import tornado.httpserver
|
||
|
import tornado.websocket
|
||
|
import tornado.ioloop
|
||
|
import tornado.web
|
||
|
import hmac
|
||
|
import sys
|
||
|
import optparse
|
||
|
import time
|
||
|
import sys
|
||
|
import gluon.utils
|
||
|
from gluon._compat import to_native, to_bytes, urlencode, urlopen
|
||
|
|
||
|
listeners, names, tokens = {}, {}, {}
|
||
|
|
||
|
def websocket_send(url, message, hmac_key=None, group='default'):
|
||
|
sig = hmac_key and hmac.new(to_bytes(hmac_key), to_bytes(message)).hexdigest() or ''
|
||
|
params = urlencode(
|
||
|
{'message': message, 'signature': sig, 'group': group})
|
||
|
f = urlopen(url, to_bytes(params))
|
||
|
data = f.read()
|
||
|
f.close()
|
||
|
return data
|
||
|
|
||
|
|
||
|
class PostHandler(tornado.web.RequestHandler):
|
||
|
"""
|
||
|
only authorized parties can post messages
|
||
|
"""
|
||
|
def post(self):
|
||
|
if hmac_key and not 'signature' in self.request.arguments:
|
||
|
self.send_error(401)
|
||
|
if 'message' in self.request.arguments:
|
||
|
message = self.request.arguments['message'][0].decode(encoding='UTF-8')
|
||
|
group = self.request.arguments.get('group', ['default'])[0].decode(encoding='UTF-8')
|
||
|
print('%s:MESSAGE to %s:%s' % (time.time(), group, message))
|
||
|
if hmac_key:
|
||
|
signature = self.request.arguments['signature'][0]
|
||
|
actual_signature = hmac.new(to_bytes(hmac_key), to_bytes(message)).hexdigest()
|
||
|
if not gluon.utils.compare(to_native(signature), actual_signature):
|
||
|
self.send_error(401)
|
||
|
for client in listeners.get(group, []):
|
||
|
client.write_message(message)
|
||
|
|
||
|
|
||
|
class TokenHandler(tornado.web.RequestHandler):
|
||
|
"""
|
||
|
if running with -t post a token to allow a client to join using the token
|
||
|
the message here is the token (any uuid)
|
||
|
allows only authorized parties to joins, for example, a chat
|
||
|
"""
|
||
|
def post(self):
|
||
|
if hmac_key and not 'message' in self.request.arguments:
|
||
|
self.send_error(401)
|
||
|
if 'message' in self.request.arguments:
|
||
|
message = self.request.arguments['message'][0]
|
||
|
if hmac_key:
|
||
|
signature = self.request.arguments['signature'][0]
|
||
|
actual_signature = hmac.new(to_bytes(hmac_key), to_bytes(message)).hexdigest()
|
||
|
if not gluon.utils.compare(to_native(signature), actual_signature):
|
||
|
self.send_error(401)
|
||
|
tokens[message] = None
|
||
|
|
||
|
|
||
|
class DistributeHandler(tornado.websocket.WebSocketHandler):
|
||
|
|
||
|
def check_origin(self, origin):
|
||
|
return True
|
||
|
|
||
|
def open(self, params):
|
||
|
group, token, name = params.split('/') + [None, None]
|
||
|
self.group = group or 'default'
|
||
|
self.token = token or 'none'
|
||
|
self.name = name or 'anonymous'
|
||
|
# only authorized parties can join
|
||
|
if DistributeHandler.tokens:
|
||
|
if not self.token in tokens or not token[self.token] is None:
|
||
|
self.close()
|
||
|
else:
|
||
|
tokens[self.token] = self
|
||
|
if not self.group in listeners:
|
||
|
listeners[self.group] = []
|
||
|
# notify clients that a member has joined the groups
|
||
|
for client in listeners.get(self.group, []):
|
||
|
client.write_message('+' + self.name)
|
||
|
listeners[self.group].append(self)
|
||
|
names[self] = self.name
|
||
|
print('%s:CONNECT to %s' % (time.time(), self.group))
|
||
|
|
||
|
def on_message(self, message):
|
||
|
pass
|
||
|
|
||
|
def on_close(self):
|
||
|
if self.group in listeners:
|
||
|
listeners[self.group].remove(self)
|
||
|
del names[self]
|
||
|
# notify clients that a member has left the groups
|
||
|
for client in listeners.get(self.group, []):
|
||
|
client.write_message('-' + self.name)
|
||
|
print('%s:DISCONNECT from %s' % (time.time(), self.group))
|
||
|
|
||
|
# if your webserver is different from tornado server uncomment this
|
||
|
# or override using something more restrictive:
|
||
|
# http://tornado.readthedocs.org/en/latest/websocket.html#tornado.websocket.WebSocketHandler.check_origin
|
||
|
# def check_origin(self, origin):
|
||
|
# return True
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
usage = __doc__
|
||
|
version = ""
|
||
|
parser = optparse.OptionParser(usage, None, optparse.Option, version)
|
||
|
parser.add_option('-p',
|
||
|
'--port',
|
||
|
default='8888',
|
||
|
dest='port',
|
||
|
help='socket')
|
||
|
parser.add_option('-l',
|
||
|
'--listen',
|
||
|
default='0.0.0.0',
|
||
|
dest='address',
|
||
|
help='listener address')
|
||
|
parser.add_option('-k',
|
||
|
'--hmac_key',
|
||
|
default='',
|
||
|
dest='hmac_key',
|
||
|
help='hmac_key')
|
||
|
parser.add_option('-t',
|
||
|
'--tokens',
|
||
|
action='store_true',
|
||
|
default=False,
|
||
|
dest='tokens',
|
||
|
help='require tockens to join')
|
||
|
parser.add_option('-s',
|
||
|
'--sslkey',
|
||
|
default=False,
|
||
|
dest='keyfile',
|
||
|
help='require ssl keyfile full path')
|
||
|
parser.add_option('-c',
|
||
|
'--sslcert',
|
||
|
default=False,
|
||
|
dest='certfile',
|
||
|
help='require ssl certfile full path')
|
||
|
(options, args) = parser.parse_args()
|
||
|
hmac_key = options.hmac_key
|
||
|
DistributeHandler.tokens = options.tokens
|
||
|
urls = [
|
||
|
(r'/', PostHandler),
|
||
|
(r'/token', TokenHandler),
|
||
|
(r'/realtime/(.*)', DistributeHandler)]
|
||
|
application = tornado.web.Application(urls, auto_reload=True)
|
||
|
if options.keyfile and options.certfile:
|
||
|
ssl_options = dict(certfile=options.certfile, keyfile=options.keyfile)
|
||
|
else:
|
||
|
ssl_options = None
|
||
|
http_server = tornado.httpserver.HTTPServer(application, ssl_options=ssl_options)
|
||
|
http_server.listen(int(options.port), address=options.address)
|
||
|
tornado.ioloop.IOLoop.instance().start()
|