1 | # -*- coding: utf-8 -*- |
---|
2 | #!/usr/bin/env python |
---|
3 | |
---|
4 | """ |
---|
5 | This file is part of the web2py Web Framework |
---|
6 | Copyrighted by Massimo Di Pierro <mdip...@cs.depaul.edu> |
---|
7 | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) |
---|
8 | |
---|
9 | Attention: Requires Chrome or Safari. For IE of Firefox you need https://github.com/gimite/web-socket-js |
---|
10 | |
---|
11 | 1) install tornado (requires Tornado 3.0 or later) |
---|
12 | |
---|
13 | easy_install tornado |
---|
14 | |
---|
15 | 2) start this app: |
---|
16 | |
---|
17 | python gluon/contrib/websocket_messaging.py -k mykey -p 8888 |
---|
18 | |
---|
19 | 3) from any web2py app you can post messages with |
---|
20 | |
---|
21 | from gluon.contrib.websocket_messaging import websocket_send |
---|
22 | websocket_send('http://127.0.0.1:8888', 'Hello World', 'mykey', 'mygroup') |
---|
23 | |
---|
24 | 4) from any template you can receive them with |
---|
25 | |
---|
26 | <script> |
---|
27 | $(document).ready(function(){ |
---|
28 | if(!$.web2py.web2py_websocket('ws://127.0.0.1:8888/realtime/mygroup', function(e){alert(e.data)})) |
---|
29 | |
---|
30 | alert("html5 websocket not supported by your browser, try Google Chrome"); |
---|
31 | }); |
---|
32 | </script> |
---|
33 | |
---|
34 | When the server posts a message, all clients connected to the page will popup an alert message |
---|
35 | Or if you want to send json messages and store evaluated json in a var called data: |
---|
36 | |
---|
37 | <script> |
---|
38 | $(document).ready(function(){ |
---|
39 | var data; |
---|
40 | $.web2py.web2py_websocket('ws://127.0.0.1:8888/realtime/mygroup', function(e){data=eval('('+e.data+')')}); |
---|
41 | }); |
---|
42 | </script> |
---|
43 | |
---|
44 | - All communications between web2py and websocket_messaging will be digitally signed with hmac. |
---|
45 | - All validation is handled on the web2py side and there is no need to modify websocket_messaging.py |
---|
46 | - Multiple web2py instances can talk with one or more websocket_messaging servers. |
---|
47 | - "ws://127.0.0.1:8888/realtime/" must be contain the IP of the websocket_messaging server. |
---|
48 | - Via group='mygroup' name you can support multiple groups of clients (think of many chat-rooms) |
---|
49 | |
---|
50 | |
---|
51 | Here is a complete sample web2py action: |
---|
52 | |
---|
53 | def index(): |
---|
54 | form=LOAD('default', 'ajax_form', ajax=True) |
---|
55 | script=SCRIPT(''' |
---|
56 | jQuery(document).ready(function(){ |
---|
57 | var callback=function(e){alert(e.data)}; |
---|
58 | if(!$.web2py.web2py_websocket('ws://127.0.0.1:8888/realtime/mygroup', callback)) |
---|
59 | |
---|
60 | alert("html5 websocket not supported by your browser, try Google Chrome"); |
---|
61 | }); |
---|
62 | ''') |
---|
63 | return dict(form=form, script=script) |
---|
64 | |
---|
65 | def ajax_form(): |
---|
66 | form=SQLFORM.factory(Field('message')) |
---|
67 | if form.accepts(request,session): |
---|
68 | from gluon.contrib.websocket_messaging import websocket_send |
---|
69 | websocket_send( |
---|
70 | 'http://127.0.0.1:8888', form.vars.message, 'mykey', 'mygroup') |
---|
71 | return form |
---|
72 | |
---|
73 | https is possible too using 'https://127.0.0.1:8888' instead of 'http://127.0.0.1:8888', but need to |
---|
74 | be started with |
---|
75 | |
---|
76 | python gluon/contrib/websocket_messaging.py -k mykey -p 8888 -s keyfile.pem -c certfile.pem |
---|
77 | |
---|
78 | for secure websocket do: |
---|
79 | |
---|
80 | web2py_websocket('wss://127.0.0.1:8888/realtime/mygroup',callback) |
---|
81 | |
---|
82 | Acknowledgements: |
---|
83 | Tornado code inspired by http://thomas.pelletier.im/2010/08/websocket-tornado-redis/ |
---|
84 | |
---|
85 | """ |
---|
86 | from __future__ import print_function |
---|
87 | import tornado.httpserver |
---|
88 | import tornado.websocket |
---|
89 | import tornado.ioloop |
---|
90 | import tornado.web |
---|
91 | import hmac |
---|
92 | import sys |
---|
93 | import optparse |
---|
94 | import time |
---|
95 | import sys |
---|
96 | import gluon.utils |
---|
97 | from gluon._compat import to_native, to_bytes, urlencode, urlopen |
---|
98 | |
---|
99 | listeners, names, tokens = {}, {}, {} |
---|
100 | |
---|
101 | def websocket_send(url, message, hmac_key=None, group='default'): |
---|
102 | sig = hmac_key and hmac.new(to_bytes(hmac_key), to_bytes(message)).hexdigest() or '' |
---|
103 | params = urlencode( |
---|
104 | {'message': message, 'signature': sig, 'group': group}) |
---|
105 | f = urlopen(url, to_bytes(params)) |
---|
106 | data = f.read() |
---|
107 | f.close() |
---|
108 | return data |
---|
109 | |
---|
110 | |
---|
111 | class PostHandler(tornado.web.RequestHandler): |
---|
112 | """ |
---|
113 | only authorized parties can post messages |
---|
114 | """ |
---|
115 | def post(self): |
---|
116 | if hmac_key and not 'signature' in self.request.arguments: |
---|
117 | self.send_error(401) |
---|
118 | if 'message' in self.request.arguments: |
---|
119 | message = self.request.arguments['message'][0].decode(encoding='UTF-8') |
---|
120 | group = self.request.arguments.get('group', ['default'])[0].decode(encoding='UTF-8') |
---|
121 | print('%s:MESSAGE to %s:%s' % (time.time(), group, message)) |
---|
122 | if hmac_key: |
---|
123 | signature = self.request.arguments['signature'][0] |
---|
124 | actual_signature = hmac.new(to_bytes(hmac_key), to_bytes(message)).hexdigest() |
---|
125 | if not gluon.utils.compare(to_native(signature), actual_signature): |
---|
126 | self.send_error(401) |
---|
127 | for client in listeners.get(group, []): |
---|
128 | client.write_message(message) |
---|
129 | |
---|
130 | |
---|
131 | class TokenHandler(tornado.web.RequestHandler): |
---|
132 | """ |
---|
133 | if running with -t post a token to allow a client to join using the token |
---|
134 | the message here is the token (any uuid) |
---|
135 | allows only authorized parties to joins, for example, a chat |
---|
136 | """ |
---|
137 | def post(self): |
---|
138 | if hmac_key and not 'message' in self.request.arguments: |
---|
139 | self.send_error(401) |
---|
140 | if 'message' in self.request.arguments: |
---|
141 | message = self.request.arguments['message'][0] |
---|
142 | if hmac_key: |
---|
143 | signature = self.request.arguments['signature'][0] |
---|
144 | actual_signature = hmac.new(to_bytes(hmac_key), to_bytes(message)).hexdigest() |
---|
145 | if not gluon.utils.compare(to_native(signature), actual_signature): |
---|
146 | self.send_error(401) |
---|
147 | tokens[message] = None |
---|
148 | |
---|
149 | |
---|
150 | class DistributeHandler(tornado.websocket.WebSocketHandler): |
---|
151 | |
---|
152 | def check_origin(self, origin): |
---|
153 | return True |
---|
154 | |
---|
155 | def open(self, params): |
---|
156 | group, token, name = params.split('/') + [None, None] |
---|
157 | self.group = group or 'default' |
---|
158 | self.token = token or 'none' |
---|
159 | self.name = name or 'anonymous' |
---|
160 | # only authorized parties can join |
---|
161 | if DistributeHandler.tokens: |
---|
162 | if not self.token in tokens or not token[self.token] is None: |
---|
163 | self.close() |
---|
164 | else: |
---|
165 | tokens[self.token] = self |
---|
166 | if not self.group in listeners: |
---|
167 | listeners[self.group] = [] |
---|
168 | # notify clients that a member has joined the groups |
---|
169 | for client in listeners.get(self.group, []): |
---|
170 | client.write_message('+' + self.name) |
---|
171 | listeners[self.group].append(self) |
---|
172 | names[self] = self.name |
---|
173 | print('%s:CONNECT to %s' % (time.time(), self.group)) |
---|
174 | |
---|
175 | def on_message(self, message): |
---|
176 | pass |
---|
177 | |
---|
178 | def on_close(self): |
---|
179 | if self.group in listeners: |
---|
180 | listeners[self.group].remove(self) |
---|
181 | del names[self] |
---|
182 | # notify clients that a member has left the groups |
---|
183 | for client in listeners.get(self.group, []): |
---|
184 | client.write_message('-' + self.name) |
---|
185 | print('%s:DISCONNECT from %s' % (time.time(), self.group)) |
---|
186 | |
---|
187 | # if your webserver is different from tornado server uncomment this |
---|
188 | # or override using something more restrictive: |
---|
189 | # http://tornado.readthedocs.org/en/latest/websocket.html#tornado.websocket.WebSocketHandler.check_origin |
---|
190 | # def check_origin(self, origin): |
---|
191 | # return True |
---|
192 | |
---|
193 | if __name__ == "__main__": |
---|
194 | usage = __doc__ |
---|
195 | version = "" |
---|
196 | parser = optparse.OptionParser(usage, None, optparse.Option, version) |
---|
197 | parser.add_option('-p', |
---|
198 | '--port', |
---|
199 | default='8888', |
---|
200 | dest='port', |
---|
201 | help='socket') |
---|
202 | parser.add_option('-l', |
---|
203 | '--listen', |
---|
204 | default='0.0.0.0', |
---|
205 | dest='address', |
---|
206 | help='listener address') |
---|
207 | parser.add_option('-k', |
---|
208 | '--hmac_key', |
---|
209 | default='', |
---|
210 | dest='hmac_key', |
---|
211 | help='hmac_key') |
---|
212 | parser.add_option('-t', |
---|
213 | '--tokens', |
---|
214 | action='store_true', |
---|
215 | default=False, |
---|
216 | dest='tokens', |
---|
217 | help='require tockens to join') |
---|
218 | parser.add_option('-s', |
---|
219 | '--sslkey', |
---|
220 | default=False, |
---|
221 | dest='keyfile', |
---|
222 | help='require ssl keyfile full path') |
---|
223 | parser.add_option('-c', |
---|
224 | '--sslcert', |
---|
225 | default=False, |
---|
226 | dest='certfile', |
---|
227 | help='require ssl certfile full path') |
---|
228 | (options, args) = parser.parse_args() |
---|
229 | hmac_key = options.hmac_key |
---|
230 | DistributeHandler.tokens = options.tokens |
---|
231 | urls = [ |
---|
232 | (r'/', PostHandler), |
---|
233 | (r'/token', TokenHandler), |
---|
234 | (r'/realtime/(.*)', DistributeHandler)] |
---|
235 | application = tornado.web.Application(urls, auto_reload=True) |
---|
236 | if options.keyfile and options.certfile: |
---|
237 | ssl_options = dict(certfile=options.certfile, keyfile=options.keyfile) |
---|
238 | else: |
---|
239 | ssl_options = None |
---|
240 | http_server = tornado.httpserver.HTTPServer(application, ssl_options=ssl_options) |
---|
241 | http_server.listen(int(options.port), address=options.address) |
---|
242 | tornado.ioloop.IOLoop.instance().start() |
---|