Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# -*- 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
import hashlib
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), digestmod=hashlib.md5).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), digestmod=hashlib.md5).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), digestmod=hashlib.md5).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()