Completed
Push — master ( 7fb4de...e21d8c )
by Andrew
58s
created

MessagesCreator.get_miliseconds()   A

Complexity

Conditions 3

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 3
dl 0
loc 8
rs 9.4286
1
import json
2
import logging
3
import sys
4
from threading import Thread
5
from  urllib.request import urlopen
6
7
import redis
8
import time
9
import tornado.gen
10
import tornado.httpclient
11
import tornado.ioloop
12
import tornado.web
13
import tornado.websocket
14
import tornadoredis
15
from django.conf import settings
16
from django.core.exceptions import ValidationError
17
from django.db import connection, OperationalError, InterfaceError
18
from django.db.models import Q
19
from redis_sessions.session import SessionStore
20
from tornado.websocket import WebSocketHandler
21
22
from chat.log_filters import id_generator
23
24
try:
25
	from urllib.parse import urlparse  # py2
26
except ImportError:
27
	from urlparse import urlparse  # py3
28
29
from chat.settings import MAX_MESSAGE_SIZE, ANONYMOUS_REDIS_ROOM
30
from chat.models import User, Message, Room, IpAddress, get_milliseconds
31
from chat.utils import check_user
32
33
PY3 = sys.version > '3'
34
35
user_cookie_name = settings.USER_COOKIE_NAME
36
api_url = getattr(settings, "IP_API_URL", None)
37
38
ANONYMOUS_GENDER = 'Alien'
39
SESSION_USER_VAR_KEY = 'user_name'
40
41
MESSAGE_ID_VAR_NAME = 'id'
42
RECEIVER_USERNAME_VAR_NAME = 'receiverName'
43
RECEIVER_USERID_VAR_NAME = 'receiverId'
44
COUNT_VAR_NAME = 'count'
45
HEADER_ID_VAR_NAME = 'headerId'
46
USER_VAR_NAME = 'user'
47
USER_ID_VAR_NAME = 'userId'
48
TIME_VAR_NAME = 'time'
49
OLD_NAME_VAR_NAME = 'oldName'
50
IS_ANONYMOUS_VAR_NAME = 'anonymous'
51
CONTENT_VAR_NAME = 'content'
52
EVENT_VAR_NAME = 'action'
53
GENDER_VAR_NAME = 'sex'
54
55
REFRESH_USER_EVENT = 'onlineUsers'
56
SYSTEM_MESSAGE_EVENT = 'system'
57
GET_MESSAGES_EVENT = 'messages'
58
GET_MINE_USERNAME_EVENT = 'me'
59
ROOMS_EVENT = 'rooms'  # thread ex "main" , channel ex. 'r:main', "i:3"
60
LOGIN_EVENT = 'joined'
61
LOGOUT_EVENT = 'left'
62
SEND_MESSAGE_EVENT = 'send'
63
CHANGE_ANONYMOUS_NAME_EVENT = 'changed'
64
65
REDIS_USERNAME_CHANNEL_PREFIX = 'u:%s'
66
REDIS_USERID_CHANNEL_PREFIX = 'i:%s'
67
REDIS_ROOM_CHANNEL_PREFIX = 'r:%d'
68
REDIS_ONLINE_USERS = "online_users"
69
70
71
# global connection to read synchronously
72
sync_redis = redis.StrictRedis()
73
# Redis connection cannot be shared between publishers and subscribers.
74
async_redis_publisher = tornadoredis.Client()
75
async_redis_publisher.connect()
76
sync_redis.delete(REDIS_ONLINE_USERS)  # TODO move it somewhere else
77
78
try:
79
	anonymous_default_room = Room.objects.get(name=ANONYMOUS_REDIS_ROOM)
80
except Room.DoesNotExist:
81
	anonymous_default_room = Room()
82
	anonymous_default_room.name = ANONYMOUS_REDIS_ROOM
83
	anonymous_default_room.save()
84
85
ANONYMOUS_REDIS_CHANNEL = REDIS_ROOM_CHANNEL_PREFIX % anonymous_default_room.id
86
ANONYMOUS_ROOM_NAMES = {anonymous_default_room.id: anonymous_default_room.name}
87
88
sessionStore = SessionStore()
89
90
logger = logging.getLogger(__name__)
91
92
# TODO https://github.com/leporo/tornado-redis#connection-pool-support
93
CONNECTION_POOL = tornadoredis.ConnectionPool(
94
	max_connections=500,
95
	wait_for_available=True)
96
97
98
class MessagesCreator(object):
99
100
	def __init__(self, *args, **kwargs):
101
		super(MessagesCreator, self).__init__(*args, **kwargs)
102
		self.sex = ANONYMOUS_GENDER
103
		self.sender_name = None
104
		self.user_id = 0  # anonymous by default
105
106
	def online_user_names(self, user_names_dict, action):
107
		"""
108
		:type user_names_dict: dict
109
		:return: { Nick: male, NewName: alien, Joana: female}
110
		"""
111
		default_message = self.default(user_names_dict, action)
112
		default_message.update({
113
			USER_VAR_NAME: self.sender_name,
114
			IS_ANONYMOUS_VAR_NAME: self.sex == ANONYMOUS_GENDER
115
		})
116
		return default_message
117
118
	def change_user_nickname(self, old_nickname, online):
119
		"""
120
		set self.sender_name to new nickname before call it
121
		:return: {action : changed, content: { Nick: male, NewName: alien}, oldName : OldName, user: NewName}
122
		:type old_nickname: str
123
		:type online: dict
124
		"""
125
		default_message = self.online_user_names(online, CHANGE_ANONYMOUS_NAME_EVENT)
126
		default_message[OLD_NAME_VAR_NAME] = old_nickname
127
		return default_message
128
129
	@classmethod
130
	def default(cls, content, event=SYSTEM_MESSAGE_EVENT):
131
		"""
132
		:return: {"action": event, "content": content, "time": "20:48:57"}
133
		"""
134
		return {
135
			EVENT_VAR_NAME: event,
136
			CONTENT_VAR_NAME: content,
137
			TIME_VAR_NAME: get_milliseconds()
138
		}
139
140
	@classmethod
141
	def create_send_message(cls, message):
142
		"""
143
		:type message: Message
144
		"""
145
		result = cls.get_message(message)
146
		result[EVENT_VAR_NAME] = SEND_MESSAGE_EVENT
147
		return result
148
149
	@classmethod
150
	def get_message(cls, message):
151
		"""
152
		:param message:
153
		:return: "action": "joined", "content": {"v5bQwtWp": "alien", "tRD6emzs": "Alien"},
154
		"sex": "Alien", "user": "tRD6emzs", "time": "20:48:57"}
155
		"""
156
		result = {
157
			USER_VAR_NAME: message.sender.username,
158
			USER_ID_VAR_NAME: message.sender.id,
159
			CONTENT_VAR_NAME: message.content,
160
			TIME_VAR_NAME: message.time,
161
			MESSAGE_ID_VAR_NAME: message.id,
162
		}
163
		if message.receiver is not None:
164
			result[RECEIVER_USERNAME_VAR_NAME] = message.receiver.username
165
		return result
166
167
	@classmethod
168
	def get_messages(cls, messages):
169
		"""
170
		:type messages: list[Messages]
171
		:type messages: QuerySet[Messages]
172
		"""
173
		return {
174
			CONTENT_VAR_NAME: [cls.create_send_message(message) for message in messages],
175
			EVENT_VAR_NAME: GET_MESSAGES_EVENT
176
		}
177
178
	def send_anonymous(self, content, receiver_anonymous, receiver_id):
179
		default_message = self.default(content, SEND_MESSAGE_EVENT)
180
		default_message[USER_VAR_NAME] = self.sender_name
181
		if receiver_anonymous is not None:
182
			default_message[RECEIVER_USERNAME_VAR_NAME] = receiver_anonymous
183
			default_message[RECEIVER_USERID_VAR_NAME] = receiver_id
184
		return default_message
185
186
	@property
187
	def stored_redis_user(self):
188
		return '%s:%s:%d' % (self.sender_name, self.sex, self.user_id)
189
190
	@property
191
	def channel(self):
192
		if self.user_id == 0:
193
			return REDIS_USERNAME_CHANNEL_PREFIX % self.sender_name
194
		else:
195
			return REDIS_USERID_CHANNEL_PREFIX % self.user_id
196
197
	@staticmethod
198
	def online_js_structure(name, sex, user_id):
199
		return {
200
			name: {
201
				GENDER_VAR_NAME: sex,
202
				USER_ID_VAR_NAME: user_id
203
			}
204
		}
205
206
	@property
207
	def online_self_js_structure(self):
208
		return self.online_js_structure(self.sender_name, self.sex, self.user_id)
209
210
211
class MessagesHandler(MessagesCreator):
212
213
	def __init__(self, *args, **kwargs):
214
		super(MessagesHandler, self).__init__(*args, **kwargs)
215
		self.log_id = str(id(self) % 10000).rjust(4, '0')
216
		self.ip = None
217
		log_params = {
218
			'username': '00000000',
219
			'id': self.log_id,
220
			'ip': 'initializing'
221
		}
222
		self.logger = logging.LoggerAdapter(logger, log_params)
223
		self.async_redis = tornadoredis.Client()
224
		self.process_message = {
225
			GET_MINE_USERNAME_EVENT: self.process_change_username,
226
			GET_MESSAGES_EVENT: self.process_get_messages,
227
			SEND_MESSAGE_EVENT: self.process_send_message,
228
		}
229
230
	def do_db(self, callback, *arg, **args):
231
		try:
232
			return callback(*arg, **args)
233
		except (OperationalError, InterfaceError) as e:  # Connection has gone away
234
			self.logger.warning('%s, reconnecting' % e)  # TODO
235
			connection.close()
236
			return callback( *arg, **args)
237
238
	def get_online_from_redis(self, check_name=None, check_id=None):
239
		"""
240
		:rtype : dict
241
		returns (dict, bool) if check_type is present
242
		"""
243
		online = sync_redis.hgetall(REDIS_ONLINE_USERS)
244
		self.logger.debug('!! redis online: %s', online)
245
		result = {}
246
		user_is_online = False
247
		# redis stores REDIS_USER_FORMAT, so parse them
248
		if online:
249
			for key, raw_user_sex in online.items():  # py2 iteritems
250
				(name, sex, user_id) = raw_user_sex.decode('utf-8').split(':')
251
				if name == check_name and check_id != int(key.decode('utf-8')):
252
					user_is_online = True
253
				result.update(self.online_js_structure(name, sex, user_id))
254
		if check_id:
255
			return result, user_is_online
256
		else:
257
			return result
258
259
	def add_online_user(self):
260
		"""
261
		adds to redis
262
		online_users = { connection_hash1 = stored_redis_user1, connection_hash_2 = stored_redis_user2 }
263
		:return:
264
		"""
265
		online = self.get_online_from_redis()
266
		async_redis_publisher.hset(REDIS_ONLINE_USERS, id(self), self.stored_redis_user)
267
		first_tab = False
268
		if self.sender_name not in online:  # if a new tab has been opened
269
			online.update(self.online_self_js_structure)
270
			first_tab = True
271
272
		if first_tab:  # Login event, sent user names to all
273
			online_user_names_mes = self.online_user_names(online, LOGIN_EVENT)
274
			self.logger.info('!! First tab, sending refresh online for all')
275
			self.publish(online_user_names_mes)
276
		else:  # Send user names to self
277
			online_user_names_mes = self.online_user_names(online, REFRESH_USER_EVENT)
278
			self.logger.info('!! Second tab, retrieving online for self')
279
			self.safe_write(online_user_names_mes)
280
		# send usernamechat
281
		username_message = self.default(self.sender_name, GET_MINE_USERNAME_EVENT)
282
		self.safe_write(username_message)
283
284
	def set_username(self, session_key):
285
		"""
286
		Case registered: Fetch userName and its channels from database. returns them
287
		Case anonymous: generates a new name and saves it to session. returns default channel
288
		:return: channels user should subscribe
289
		"""
290
		session = SessionStore(session_key)
291
		try:
292
			self.user_id = int(session["_auth_user_id"])
293
			user_db = self.do_db(User.objects.get, id=self.user_id)  # everything but 0 is a registered user
294
			self.sender_name = user_db.username
295
			self.sex = user_db.sex_str
296
			rooms = user_db.rooms.all()  # do_db is used already
297
			room_names = {}
298
			channels = [self.channel, ]
299
			for room in rooms:
300
				room_names[room.id] = room.name
301
				channels.append(REDIS_ROOM_CHANNEL_PREFIX % room.id)
302
			rooms_message = self.default(room_names, ROOMS_EVENT)
303
			self.logger.info("!! User %s subscribes for %s", self.sender_name, room_names)
304
		except (KeyError, User.DoesNotExist):
305
			# Anonymous
306
			self.sender_name = session.get(SESSION_USER_VAR_KEY)
307
			if self.sender_name is None:
308
				self.sender_name = id_generator(8)
309
				session[SESSION_USER_VAR_KEY] = self.sender_name
310
				session.save()
311
				self.logger.info("!! A new user log in, created username %s", self.sender_name)
312
			else:
313
				self.logger.info("!! Anonymous with name %s has logged", self.sender_name)
314
			channels = [ANONYMOUS_REDIS_CHANNEL, self.channel]
315
			rooms_message = self.default(ANONYMOUS_ROOM_NAMES, ROOMS_EVENT)
316
		finally:
317
			self.safe_write(rooms_message)
318
			return channels
319
320
	def publish(self, message, channel=ANONYMOUS_REDIS_CHANNEL):
321
		jsoned_mess = json.dumps(message)
322
		self.logger.debug('<%s> %s', channel, jsoned_mess)
323
		async_redis_publisher.publish(channel, jsoned_mess)
324
325
	# TODO really parse every single message for 1 action?
326
	def check_and_finish_change_name(self, message):
327
		if self.sex == ANONYMOUS_GENDER:
328
			parsed_message = json.loads(message)
329
			if parsed_message[EVENT_VAR_NAME] == GET_MINE_USERNAME_EVENT:
330
				self.async_redis.unsubscribe(REDIS_USERNAME_CHANNEL_PREFIX % self.sender_name)  # TODO is it allowed?
331
				self.sender_name = parsed_message[CONTENT_VAR_NAME]
332
				self.async_redis.subscribe(REDIS_USERNAME_CHANNEL_PREFIX % self.sender_name)
333
				async_redis_publisher.hset(REDIS_ONLINE_USERS, id(self), self.stored_redis_user)
334
335
	def new_message(self, message):
336
		if type(message.body) is not int:  # subscribe event
337
			self.safe_write(message.body)
338
			self.check_and_finish_change_name(message.body)
339
340
	def safe_write(self, message):
341
		raise NotImplementedError('WebSocketHandler implements')
342
343
	def process_send_message(self, message):
344
		"""
345
		:type message: dict
346
		"""
347
		content = message[CONTENT_VAR_NAME]
348
		receiver_id = message.get(RECEIVER_USERID_VAR_NAME)  # if receiver_id is None then its a private message
349
		receiver_name = message.get(RECEIVER_USERNAME_VAR_NAME)
350
		self.logger.info('!! Sending message %s to username:%s, id:%s', content, receiver_name, receiver_id)
351
		save_to_db = True
352
		if receiver_id is not None and receiver_id != 0:
353
			receiver_channel = REDIS_USERID_CHANNEL_PREFIX % receiver_id
354
		elif receiver_name is not None:
355
			receiver_channel = REDIS_USERNAME_CHANNEL_PREFIX % receiver_name
356
			save_to_db = False
357
358
		if self.user_id != 0 and save_to_db:
359
			self.logger.debug('!! Saving it to db')
360
			message_db = Message(sender_id=self.user_id, content=content, receiver_id=receiver_id)
361
			self.do_db(message_db.save)  # exit on hacked id with exception
362
			prepared_message = self.create_send_message(message_db)
363
		else:
364
			self.logger.debug('!! NOT saving it')
365
			prepared_message = self.send_anonymous(content, receiver_name, receiver_id)
366
367
		if receiver_id is None:
368
			self.logger.debug('!! Detected as public')
369
			self.publish(prepared_message)
370
		else:
371
			self.publish(prepared_message, self.channel)
372
			self.logger.debug('!! Detected as private, channel %s', receiver_channel)
373
			if receiver_channel != self.channel:
374
				self.publish(prepared_message, receiver_channel)
375
376
	def process_change_username(self, message):
377
		"""
378
		:type message: dict
379
		"""
380
		self.logger.info('!! Changing username to %s', message[CONTENT_VAR_NAME])
381
		new_username = message[CONTENT_VAR_NAME]
382
		try:
383
			check_user(new_username)
384
			online = self.get_online_from_redis()
385
			if new_username in online:
386
				self.logger.info('!! This name is already used')
387
				raise ValidationError('Anonymous already has this name')
388
			session_key = self.get_cookie(settings.SESSION_COOKIE_NAME)
389
			session = SessionStore(session_key)
390
			session[SESSION_USER_VAR_KEY] = new_username
391
			session.save()
392
393
			del online[self.sender_name]
394
			old_name = self.sender_name
395
			old_channel = self.channel
396
			self.sender_name = new_username  # change_user_name required new_username in sender_name
397
			online.update(self.online_self_js_structure)
398
			message_all = self.change_user_nickname(old_name, online)
399
			message_me = self.default(new_username, GET_MINE_USERNAME_EVENT)
400
			# TODO perform ton of checks or emit twice ?
401
			self.publish(message_me, self.channel)  # to new user channel
402
			self.publish(message_me, old_channel)  # to old user channel
403
			self.publish(message_all)
404
		except ValidationError as e:
405
			self.safe_write(self.default(str(e.message)))
406
407
	def process_get_messages(self, data):
408
		"""
409
		:type data: dict
410
		"""
411
		header_id = data.get(HEADER_ID_VAR_NAME, None)
412
		count = int(data.get(COUNT_VAR_NAME, 10))
413
		self.logger.info('!! Fetching %d messages starting from %s', count, header_id)
414
		if header_id is None:
415
			messages = Message.objects.filter(
416
				Q(receiver=None)  # Only public
417
				| Q(sender=self.user_id)  # private s
418
				| Q(receiver=self.user_id)  # and private
419
			).order_by('-pk')[:count]
420
		else:
421
			messages = Message.objects.filter(
422
				Q(id__lt=header_id),
423
				Q(receiver=None)
424
				| Q(sender=self.user_id)
425
				| Q(receiver=self.user_id)
426
			).order_by('-pk')[:count]
427
		response = self.do_db(self.get_messages, messages)
428
		self.safe_write(response)
429
430
	def save_ip(self):
431
		user_id = None if self.user_id == 0 else self.user_id
432
		anon_name = self.sender_name if self.user_id == 0 else None
433
		if ((user_id and self.do_db(IpAddress.objects.filter(user_id=user_id, ip=self.ip).exists)) or
434
				# << ip record exist for existed user, ip record exist for anonymous >>
435
				(anon_name and self.do_db(IpAddress.objects.filter(anon_name=anon_name, ip=self.ip).exists))):
436
			return
437
		try:
438
			if not api_url:
439
				raise Exception('api url is absent')
440
			self.logger.debug("Creating ip record %s", self.ip)
441
			f = urlopen(api_url % self.ip)
442
			raw_response = f.read().decode("utf-8")
443
			response = json.loads(raw_response)
444
			if response['status'] != "success":
445
				raise Exception("Creating iprecord failed, server responded: %s" % raw_response)
446
			IpAddress.objects.create(
447
				isp=response['isp'],
448
				country=response['country'],
449
				region=response['regionName'],
450
				city=response['city'],
451
				user_id=user_id,
452
				anon_name=anon_name,
453
				ip=self.ip)
454
		except Exception as e:
455
			self.logger.error("Error while creating ip with country info, because %s", e)
456
			IpAddress.objects.create(user_id=user_id, ip=self.ip, anon_name=anon_name)
457
458
459
class AntiSpam:
460
461
	def __init__(self):
462
		self.spammed = 0
463
		self.info = {}
464
465
	def check_spam(self, json_message):
466
		message_length = len(json_message)
467
		info_key = int(round(time.time() * 100))
468
		self.info[info_key] = message_length
469
		if message_length > MAX_MESSAGE_SIZE:
470
			self.spammed += 1
471
			raise ValidationError("Message can't exceed %s symbols" % MAX_MESSAGE_SIZE)
472
		self.check_timed_spam()
473
474
	def check_timed_spam(self):
475
		# TODO implement me
476
		pass
477
		# raise ValidationError("You're chatting too much, calm down a bit!")
478
479
480
class TornadoHandler(WebSocketHandler, MessagesHandler):
481
482
	def __init__(self, *args, **kwargs):
483
		super(TornadoHandler, self).__init__(*args, **kwargs)
484
		self.connected = False
485
		self.anti_spam = AntiSpam()
486
487
	@tornado.gen.engine
488
	def listen(self, channels):
489
		"""
490
		self.channel should been set before calling
491
		"""
492
		yield tornado.gen.Task(
493
			self.async_redis.subscribe, channels)
494
		self.async_redis.listen(self.new_message)
495
496
	def data_received(self, chunk):
497
		pass
498
499
	def on_message(self, json_message):
500
		try:
501
			if not self.connected:
502
				raise ValidationError('Skipping message %s, as websocket is not initialized yet' % json_message)
503
			if not json_message:
504
				raise ValidationError('Skipping null message')
505
			self.anti_spam.check_spam(json_message)
506
			self.logger.debug('<< %s', json_message)
507
			message = json.loads(json_message)
508
			self.process_message[message[EVENT_VAR_NAME]](message)
509
		except ValidationError as e:
510
			logger.warning("Message won't be send. Reason: %s", e.message)
511
			self.safe_write(self.default(e.message))
512
513
	def on_close(self):
514
		try:
515
			self_id = id(self)
516
			async_redis_publisher.hdel(REDIS_ONLINE_USERS, self_id)
517
			if self.connected:
518
				# seems like async solves problem with connection lost and wrong data status
519
				# http://programmers.stackexchange.com/questions/294663/how-to-store-online-status
520
				online, is_online = self.get_online_from_redis(self.sender_name, self_id)
521
				self.logger.info('!! Closing connection, redis current online %s', online)
522
				if not is_online:
523
					message = self.online_user_names(online, LOGOUT_EVENT)
524
					self.logger.debug('!! User closed the last tab, refreshing online for all')
525
					self.publish(message)
526
				else:
527
					self.logger.debug('!! User is still online in other tabs')
528
			else:
529
				self.logger.warning('Dropping connection for not connected user')
530
		finally:
531
			if self.async_redis.subscribed:
532
				#  TODO unsubscribe of all subscribed                  !IMPORTANT
533
				self.async_redis.unsubscribe([
534
					ANONYMOUS_REDIS_CHANNEL,
535
					self.channel
536
				])
537
			self.async_redis.disconnect()
538
539
	def open(self, *args, **kargs):
540
		session_key = self.get_cookie(settings.SESSION_COOKIE_NAME)
541
		if sessionStore.exists(session_key):
542
			self.logger.debug("!! Incoming connection, session %s, thread hash %s", session_key, id(self))
543
			self.async_redis.connect()
544
			channels = self.set_username(session_key)
545
			self.ip = self.get_client_ip()
546
			log_params = {
547
				'username': self.sender_name.rjust(8),
548
				'id': self.log_id,
549
				'ip': self.ip
550
			}
551
			self.logger = logging.LoggerAdapter(logger, log_params)
552
			self.listen(channels)
553
			self.add_online_user()
554
			self.connected = True
555
			Thread(target=self.save_ip).start()
556
		else:
557
			self.logger.warning('!! Session key %s has been rejected', str(session_key))
558
			self.close(403, "Session key %s has been rejected" % session_key)
559
560
561
	def check_origin(self, origin):
562
		"""
563
		check whether browser set domain matches origin
564
		"""
565
		parsed_origin = urlparse(origin)
566
		origin = parsed_origin.netloc
567
		origin_domain = origin.split(':')[0].lower()
568
		browser_set = self.request.headers.get("Host")
569
		browser_domain = browser_set.split(':')[0]
570
		return browser_domain == origin_domain
571
572
	def safe_write(self, message):
573
		"""
574
		Tries to send message, doesn't throw exception outside
575
		:type self: MessagesHandler
576
		"""
577
		try:
578
			if isinstance(message, dict):
579
				message = json.dumps(message)
580
			if not (isinstance(message, str) or (not PY3 and isinstance(message, unicode))):
581
				raise ValueError('Wrong message type : %s' % str(message))
582
			self.logger.debug(">> %s", message)
583
			self.write_message(message)
584
		except tornado.websocket.WebSocketClosedError as e:
585
			self.logger.error("%s. Can't send << %s >> message", e, str(message))
586
587
	def get_client_ip(self):
588
		x_real_ip = self.request.headers.get("X-Real-IP")
589
		return x_real_ip or self.request.remote_ip
590
591
application = tornado.web.Application([
592
	(r'.*', TornadoHandler),
593
])
594