ibt2.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """I'll Be There, 2 (ibt2) - an oversimplified attendees registration system.
  4. Copyright 2016-2019 Davide Alberani <da@erlug.linux.it>
  5. RaspiBO <info@raspibo.org>
  6. Licensed under the Apache License, Version 2.0 (the "License");
  7. you may not use this file except in compliance with the License.
  8. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
  9. Unless required by applicable law or agreed to in writing, software
  10. distributed under the License is distributed on an "AS IS" BASIS,
  11. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. See the License for the specific language governing permissions and
  13. limitations under the License.
  14. """
  15. import os
  16. import re
  17. import time
  18. import logging
  19. import datetime
  20. from operator import itemgetter
  21. import itertools
  22. import tornado.httpserver
  23. import tornado.ioloop
  24. import tornado.options
  25. from tornado.options import define, options
  26. import tornado.web
  27. from tornado import gen, escape
  28. import utils
  29. import monco
  30. API_VERSION = '1.1'
  31. class BaseException(Exception):
  32. """Base class for ibt2 custom exceptions.
  33. :param message: text message
  34. :type message: str
  35. :param status: numeric http status code
  36. :type status: int"""
  37. def __init__(self, message, status=400):
  38. super(BaseException, self).__init__(message)
  39. self.message = message
  40. self.status = status
  41. class InputException(BaseException):
  42. """Exception raised by errors in input handling."""
  43. pass
  44. class BaseHandler(tornado.web.RequestHandler):
  45. """Base class for request handlers."""
  46. # Cache currently connected users.
  47. _users_cache = {}
  48. # set of documents we're managing (a collection in MongoDB or a table in a SQL database)
  49. document = None
  50. collection = None
  51. # A property to access the first value of each argument.
  52. arguments = property(lambda self: dict([(k, v[0].decode('utf-8'))
  53. for k, v in self.request.arguments.items()]))
  54. # Arguments suitable for a query on MongoDB.
  55. clean_arguments = property(lambda self: self._clean_dict(self.arguments))
  56. _re_split_salt = re.compile(r'\$(?P<salt>.+)\$(?P<hash>.+)')
  57. @property
  58. def clean_body(self):
  59. """Return a clean dictionary from a JSON body, suitable for a query on MongoDB.
  60. :returns: a clean copy of the body arguments
  61. :rtype: dict"""
  62. data = escape.json_decode(self.request.body or '{}')
  63. return self._clean_dict(data)
  64. def _clean_dict(self, data):
  65. """Filter a dictionary (in place) to remove unwanted keywords in db queries.
  66. :param data: dictionary to clean
  67. :type data: dict"""
  68. if isinstance(data, dict):
  69. for key in list(data.keys()):
  70. if (isinstance(key, str) and key.startswith('$')) or key in ('_id', 'created_by', 'created_at',
  71. 'updated_by', 'updated_at', 'isRegistered'):
  72. del data[key]
  73. return data
  74. def write_error(self, status_code, **kwargs):
  75. """Default error handler."""
  76. if isinstance(kwargs.get('exc_info', (None, None))[1], BaseException):
  77. exc = kwargs['exc_info'][1]
  78. status_code = exc.status
  79. message = exc.message
  80. else:
  81. message = 'internal error'
  82. self.build_error(message, status=status_code)
  83. def is_api(self):
  84. """Return True if the path is from an API call."""
  85. return self.request.path.startswith('/v%s' % API_VERSION)
  86. def initialize(self, **kwargs):
  87. """Add every passed (key, value) as attributes of the instance."""
  88. for key, value in kwargs.items():
  89. setattr(self, key, value)
  90. @property
  91. def current_user(self):
  92. """Retrieve current user ID from the secure cookie."""
  93. current_user = self.get_secure_cookie("user")
  94. if isinstance(current_user, bytes):
  95. current_user = current_user.decode('utf-8')
  96. return current_user
  97. @property
  98. def current_user_info(self):
  99. """Information about the current user.
  100. :returns: full information about the current user
  101. :rtype: dict"""
  102. current_user = self.current_user
  103. if current_user in self._users_cache:
  104. return self._users_cache[current_user]
  105. user_info = {}
  106. if current_user:
  107. user_info['_id'] = current_user
  108. user = self.db.getOne('users', {'_id': user_info['_id']})
  109. if user:
  110. user_info = user
  111. user_info['isRegistered'] = True
  112. self._users_cache[current_user] = user_info
  113. return user_info
  114. def is_registered(self):
  115. """Check if the current user is registered.
  116. :returns: True if a registered user is logged in
  117. :rtype: bool"""
  118. return self.current_user_info.get('isRegistered')
  119. def is_admin(self):
  120. """Check if the current user is an admin.
  121. :returns: True if the logged in user is an admin
  122. :rtype: bool"""
  123. return self.current_user_info.get('isAdmin')
  124. def user_authorized(self, username, password):
  125. """Check if a combination of username/password is valid.
  126. :param username: username or email
  127. :type username: str
  128. :param password: password
  129. :type password: str
  130. :returns: tuple like (bool_user_is_authorized, dict_user_info)
  131. :rtype: dict"""
  132. query = [{'username': username}, {'email': username}]
  133. res = self.db.query('users', query)
  134. if not res:
  135. return (False, {})
  136. user = res[0]
  137. db_password = user.get('password') or ''
  138. if not db_password:
  139. return (False, {})
  140. match = self._re_split_salt.match(db_password)
  141. if not match:
  142. return (False, {})
  143. salt = match.group('salt')
  144. if utils.hash_password(password, salt=salt) == db_password:
  145. return (True, user)
  146. return (False, {})
  147. def build_error(self, message='', status=400):
  148. """Build and write an error message.
  149. :param message: textual message
  150. :type message: str
  151. :param status: HTTP status code
  152. :type status: int
  153. """
  154. self.set_status(status)
  155. self.write({'error': True, 'message': message})
  156. def has_permission(self, owner_id):
  157. """Check if the given owner_id matches with the current user or the logged in user is an admin; if not,
  158. build an error reply.
  159. :param owner_id: owner ID to check against
  160. :type owner_id: str, ObjectId, None
  161. :returns: if the logged in user has the permission
  162. :rtype: bool"""
  163. if (owner_id and str(self.current_user) != str(owner_id) and not
  164. self.is_admin()):
  165. self.build_error(status=401, message='insufficient permissions: must be the owner or admin')
  166. return False
  167. return True
  168. def logout(self):
  169. """Remove the secure cookie used fro authentication."""
  170. if self.current_user in self._users_cache:
  171. del self._users_cache[self.current_user]
  172. self.clear_cookie("user")
  173. def add_access_info(self, doc):
  174. """Add created/updated by/at to a document (modified in place and returned).
  175. :param doc: the doc to be updated
  176. :type doc: dict
  177. :returns: the updated document
  178. :rtype: dict"""
  179. user_id = self.current_user
  180. now = datetime.datetime.utcnow()
  181. if 'created_by' not in doc:
  182. doc['created_by'] = user_id
  183. if 'created_at' not in doc:
  184. doc['created_at'] = now
  185. doc['updated_by'] = user_id
  186. doc['updated_at'] = now
  187. return doc
  188. @staticmethod
  189. def update_global_settings(db_connector, settings=None):
  190. """Update global settings from the db.
  191. :param db_connector: database connector
  192. :type db_connector: Monco instance
  193. :param settings: the dict to update (in place, and returned)
  194. :type settings: dict
  195. :returns: the updated dictionary, also modified in place
  196. :rtype: dict"""
  197. if settings is None:
  198. settings = {}
  199. settings.clear()
  200. for setting in db_connector.query('settings'):
  201. if not ('_id' in setting and 'value' in setting):
  202. continue
  203. settings[setting['_id']] = setting['value']
  204. return settings
  205. class RootHandler(BaseHandler):
  206. """Handler for the / path."""
  207. app_path = os.path.join(os.path.dirname(__file__), "dist")
  208. @gen.coroutine
  209. def get(self, *args, **kwargs):
  210. # serve the ./app/index.html file
  211. with open(self.app_path + "/index.html", 'r') as fd:
  212. self.write(fd.read())
  213. class AttendeesHandler(BaseHandler):
  214. document = 'attendee'
  215. collection = 'attendees'
  216. @gen.coroutine
  217. def get(self, id_=None, **kwargs):
  218. if id_:
  219. output = self.db.getOne(self.collection, {'_id': id_})
  220. else:
  221. output = {self.collection: self.db.query(self.collection, self.clean_arguments)}
  222. self.write(output)
  223. @gen.coroutine
  224. def post(self, **kwargs):
  225. data = self.clean_body
  226. for key in 'name', 'group', 'day':
  227. value = (data.get(key) or '').strip()
  228. if not value:
  229. return self.build_error(status=404, message="%s can't be empty" % key)
  230. data[key] = value
  231. self.add_access_info(data)
  232. doc = self.db.add(self.collection, data)
  233. self.write(doc)
  234. @gen.coroutine
  235. def put(self, id_, **kwargs):
  236. data = self.clean_body
  237. doc = self.db.getOne(self.collection, {'_id': id_}) or {}
  238. if not doc:
  239. return self.build_error(status=404, message='unable to access the resource')
  240. if not self.has_permission(doc.get('created_by')):
  241. return
  242. if self.global_settings.get('protectUnregistered') and not self.is_admin():
  243. return self.build_error(status=401, message='insufficient permissions: must be admin')
  244. self.add_access_info(data)
  245. merged, doc = self.db.update(self.collection, {'_id': id_}, data)
  246. self.write(doc)
  247. @gen.coroutine
  248. def delete(self, id_=None, **kwargs):
  249. if id_ is None:
  250. return self.build_error(status=404, message='unable to access the resource')
  251. doc = self.db.getOne(self.collection, {'_id': id_}) or {}
  252. if not doc:
  253. return self.build_error(status=404, message='unable to access the resource')
  254. if not self.has_permission(doc.get('created_by')):
  255. return
  256. if self.global_settings.get('protectUnregistered') and not self.is_admin():
  257. return self.build_error(status=401, message='insufficient permissions: must be admin')
  258. howMany = self.db.delete(self.collection, id_)
  259. self.write({'success': True, 'deleted entries': howMany.get('n')})
  260. class DaysHandler(BaseHandler):
  261. """Handle requests for Days."""
  262. def _summarize(self, days):
  263. res = []
  264. for day in days:
  265. res.append({'day': day['day'], 'groups_count': len(day.get('groups', []))})
  266. return res
  267. @gen.coroutine
  268. def get(self, day=None, **kwargs):
  269. params = self.clean_arguments
  270. summary = params.get('summary', False)
  271. if summary:
  272. del params['summary']
  273. start = params.get('start')
  274. if start:
  275. del params['start']
  276. end = params.get('end')
  277. if end:
  278. del params['end']
  279. base = {}
  280. groupsDetails = {}
  281. if day:
  282. params['day'] = day
  283. base = self.db.getOne('days', {'day': day})
  284. groupsDetails = dict([(x['group'], x) for x in self.db.query('groups', {'day': day})])
  285. else:
  286. if start:
  287. params['day'] = {'$gte': start}
  288. if end:
  289. if 'day' not in params:
  290. params['day'] = {}
  291. if end.count('-') == 0:
  292. end += '-13'
  293. elif end.count('-') == 1:
  294. end += '-31'
  295. params['day'].update({'$lte': end})
  296. results = self.db.query('attendees', params)
  297. days = []
  298. dayData = {}
  299. try:
  300. sortedDays = []
  301. for result in results:
  302. if not ('day' in result and 'group' in result and 'name' in result):
  303. self.logger.warn('unable to parse entry; dayData: %s', dayData)
  304. continue
  305. sortedDays.append(result)
  306. sortedDays = sorted(sortedDays, key=itemgetter('day'))
  307. for d, dayItems in itertools.groupby(sortedDays, key=itemgetter('day')):
  308. dayData = {'day': d, 'groups': []}
  309. for group, attendees in itertools.groupby(sorted(dayItems, key=itemgetter('group')),
  310. key=itemgetter('group')):
  311. attendees = sorted(attendees, key=itemgetter('_id'))
  312. groupData = groupsDetails.get(group) or {}
  313. groupData.update({'group': group, 'attendees': attendees})
  314. dayData['groups'].append(groupData)
  315. days.append(dayData)
  316. except Exception as e:
  317. self.logger.warn('unable to parse entry; dayData: %s error: %s', dayData, e)
  318. if summary:
  319. days = self._summarize(days)
  320. if not day:
  321. self.write({'days': days})
  322. elif days:
  323. base.update(days[0])
  324. self.write(base)
  325. else:
  326. self.write(base)
  327. class DaysInfoHandler(BaseHandler):
  328. """Handle requests for Days info."""
  329. @gen.coroutine
  330. def put(self, day='', **kwargs):
  331. day = day.strip()
  332. if not day:
  333. return self.build_error(status=404, message='unable to access the resource')
  334. data = self.clean_body
  335. if 'notes' in data and self.global_settings.get('protectDayNotes') and not self.is_admin():
  336. return self.build_error(status=401, message='insufficient permissions: must be admin')
  337. data['day'] = day
  338. self.add_access_info(data)
  339. merged, doc = self.db.update('days', {'day': day}, data)
  340. self.write(doc)
  341. class GroupsHandler(BaseHandler):
  342. """Handle requests for Groups."""
  343. @gen.coroutine
  344. def put(self, day='', group='', **kwargs):
  345. day = day.strip()
  346. group = group.strip()
  347. if not (day and group):
  348. return self.build_error(status=404, message='unable to access the resource')
  349. data = self.clean_body
  350. newName = (data.get('newName') or '').strip()
  351. if newName:
  352. if self.global_settings.get('protectGroupName') and not self.is_admin():
  353. return self.build_error(status=401, message='insufficient permissions: must be admin')
  354. query = {'day': day, 'group': group}
  355. data = {'group': newName}
  356. self.db.updateMany('attendees', query, data)
  357. self.db.updateMany('groups', query, data)
  358. self.write({'success': True})
  359. else:
  360. self.write({'success': False})
  361. @gen.coroutine
  362. def delete(self, day='', group='', **kwargs):
  363. day = day.strip()
  364. group = group.strip()
  365. if not (day and group):
  366. return self.build_error(status=404, message='unable to access the resource')
  367. if not self.is_admin():
  368. return self.build_error(status=401, message='insufficient permissions: must be admin')
  369. query = {'day': day, 'group': group}
  370. howMany = self.db.delete('attendees', query)
  371. self.db.delete('groups', query)
  372. self.write({'success': True, 'deleted entries': howMany.get('n')})
  373. class GroupsInfoHandler(BaseHandler):
  374. """Handle requests for Groups Info."""
  375. @gen.coroutine
  376. def put(self, day='', group='', **kwargs):
  377. day = day.strip()
  378. group = group.strip()
  379. if not (day and group):
  380. return self.build_error(status=404, message='unable to access the resource')
  381. data = self.clean_body
  382. if 'notes' in data and self.global_settings.get('protectGroupNotes') and not self.is_admin():
  383. return self.build_error(status=401, message='insufficient permissions: must be admin')
  384. data['day'] = day
  385. data['group'] = group
  386. self.add_access_info(data)
  387. merged, doc = self.db.update('groups', {'day': day, 'group': group}, data)
  388. self.write(doc)
  389. class UsersHandler(BaseHandler):
  390. """Handle requests for Users."""
  391. document = 'user'
  392. collection = 'users'
  393. @gen.coroutine
  394. def get(self, id_=None, **kwargs):
  395. if id_:
  396. if not self.has_permission(id_):
  397. return
  398. output = self.db.getOne(self.collection, {'_id': id_})
  399. if 'password' in output:
  400. del output['password']
  401. else:
  402. if not self.is_admin():
  403. return self.build_error(status=401, message='insufficient permissions: must be an admin')
  404. output = {self.collection: self.db.query(self.collection, self.clean_arguments)}
  405. for user in output['users']:
  406. if 'password' in user:
  407. del user['password']
  408. self.write(output)
  409. @gen.coroutine
  410. def post(self, **kwargs):
  411. data = self.clean_body
  412. username = (data.get('username') or '').strip()
  413. password = (data.get('password') or '').strip()
  414. email = (data.get('email') or '').strip()
  415. if not (username and password):
  416. raise InputException('missing username or password')
  417. res = self.db.query('users', {'username': username})
  418. if res:
  419. raise InputException('username already exists')
  420. data['username'] = username
  421. data['password'] = utils.hash_password(password)
  422. data['email'] = email
  423. self.add_access_info(data)
  424. if 'isAdmin' in data and not self.is_admin():
  425. del data['isAdmin']
  426. doc = self.db.add(self.collection, data)
  427. if 'password' in doc:
  428. del doc['password']
  429. self.write(doc)
  430. @gen.coroutine
  431. def put(self, id_=None, **kwargs):
  432. data = self.clean_body
  433. if id_ is None:
  434. return self.build_error(status=404, message='unable to access the resource')
  435. if not self.has_permission(id_):
  436. return
  437. if 'username' in data:
  438. del data['username']
  439. if 'isAdmin' in data and (str(self.current_user) == id_ or not self.is_admin()):
  440. del data['isAdmin']
  441. if 'password' in data:
  442. password = (data['password'] or '').strip()
  443. if password:
  444. data['password'] = utils.hash_password(password)
  445. else:
  446. del data['password']
  447. self.add_access_info(data)
  448. merged, doc = self.db.update(self.collection, {'_id': id_}, data)
  449. if 'password' in doc:
  450. del doc['password']
  451. self.write(doc)
  452. @gen.coroutine
  453. def delete(self, id_=None, **kwargs):
  454. if id_ is None:
  455. return self.build_error(status=404, message='unable to access the resource')
  456. if not self.has_permission(id_):
  457. return self.build_error(status=401, message='insufficient permissions: must be admin')
  458. if id_ == self.current_user:
  459. return self.build_error(status=401, message='unable to delete the current user; ask an admin')
  460. doc = self.db.getOne(self.collection, {'_id': id_})
  461. if not doc:
  462. return self.build_error(status=404, message='unable to access the resource')
  463. if doc.get('username') == 'admin':
  464. return self.build_error(status=401, message='unable to delete the admin user')
  465. howMany = self.db.delete(self.collection, id_)
  466. if id_ in self._users_cache:
  467. del self._users_cache[id_]
  468. self.write({'success': True, 'deleted entries': howMany.get('n')})
  469. class CurrentUserHandler(BaseHandler):
  470. """Handle requests for information about the logged in user."""
  471. @gen.coroutine
  472. def get(self, **kwargs):
  473. user_info = self.current_user_info or {}
  474. if 'password' in user_info:
  475. del user_info['password']
  476. self.write(user_info)
  477. class SettingsHandler(BaseHandler):
  478. """Handle global settings."""
  479. collection = 'settings'
  480. def get(self, id_=None):
  481. query = {}
  482. if id_ is not None:
  483. query['_id'] = id_
  484. res = self.db.query(self.collection, query)
  485. res = dict((i.get('_id'), i.get('value')) for i in res if '_id' in i and isinstance(i.get('_id'), str))
  486. if id_ is not None:
  487. res = {id_: res.get(id_)}
  488. self.write(res)
  489. def post(self, id_=None):
  490. if not self.is_admin():
  491. return self.build_error(status=401, message='insufficient permissions: must be an admin')
  492. data = self.clean_body
  493. if id_ is not None:
  494. # if we access a specific resource, we assume the data is in {_id: value} format
  495. if id_ not in data:
  496. return self.build_error(status=404, message='incomplete data')
  497. return
  498. data = {id_: data[id_]}
  499. for key, value in data.items():
  500. if self.db.get(self.collection, key):
  501. self.db.update(self.collection, {'_id': key}, {'value': value})
  502. else:
  503. self.db.add(self.collection, {'_id': key, 'value': value})
  504. settings = SettingsHandler.update_global_settings(self.db, self.global_settings)
  505. self.write(settings)
  506. put = post
  507. class LoginHandler(RootHandler):
  508. """Handle user authentication requests."""
  509. @gen.coroutine
  510. def get(self, **kwargs):
  511. # show the login page
  512. if self.is_api():
  513. self.set_status(401)
  514. self.write({'error': True,
  515. 'message': 'authentication required'})
  516. @gen.coroutine
  517. def post(self, *args, **kwargs):
  518. # authenticate a user
  519. try:
  520. password = self.get_body_argument('password')
  521. username = self.get_body_argument('username')
  522. except tornado.web.MissingArgumentError:
  523. data = self.clean_body
  524. username = data.get('username')
  525. password = data.get('password')
  526. if not (username and password):
  527. self.set_status(401)
  528. self.write({'error': True, 'message': 'missing username or password'})
  529. return
  530. authorized, user = self.user_authorized(username, password)
  531. if authorized and 'username' in user and '_id' in user:
  532. id_ = str(user['_id'])
  533. username = user['username']
  534. logging.info('successful login for user %s (id: %s)' % (username, id_))
  535. self.set_secure_cookie("user", id_)
  536. user_info = self.current_user_info
  537. if 'password' in user_info:
  538. del user_info['password']
  539. self.write(user_info)
  540. return
  541. logging.info('login failed for user %s' % username)
  542. self.set_status(401)
  543. self.write({'error': True, 'message': 'wrong username and password'})
  544. class LogoutHandler(BaseHandler):
  545. """Handle user logout requests."""
  546. @gen.coroutine
  547. def get(self, **kwargs):
  548. # log the user out
  549. logging.info('logout')
  550. self.logout()
  551. self.write({'error': False, 'message': 'logged out'})
  552. def run():
  553. """Run the Tornado web application."""
  554. # command line arguments; can also be written in a configuration file,
  555. # specified with the --config argument.
  556. define("port", default=3000, help="run on the given port", type=int)
  557. define("address", default='', help="bind the server at the given address", type=str)
  558. define("ssl_cert", default=os.path.join(os.path.dirname(__file__), 'ssl', 'ibt2_cert.pem'),
  559. help="specify the SSL certificate to use for secure connections")
  560. define("ssl_key", default=os.path.join(os.path.dirname(__file__), 'ssl', 'ibt2_key.pem'),
  561. help="specify the SSL private key to use for secure connections")
  562. define("mongo_url", default=None,
  563. help="URL to MongoDB server", type=str)
  564. define("db_name", default='ibt2',
  565. help="Name of the MongoDB database to use", type=str)
  566. define("debug", default=False, help="run in debug mode")
  567. define("config", help="read configuration file",
  568. callback=lambda path: tornado.options.parse_config_file(path, final=False))
  569. tornado.options.parse_command_line()
  570. logger = logging.getLogger()
  571. logger.setLevel(logging.INFO)
  572. if options.debug:
  573. logger.setLevel(logging.DEBUG)
  574. ssl_options = {}
  575. if os.path.isfile(options.ssl_key) and os.path.isfile(options.ssl_cert):
  576. ssl_options = dict(certfile=options.ssl_cert, keyfile=options.ssl_key)
  577. # database backend connector
  578. _count = 0
  579. while True:
  580. try:
  581. db_connector = monco.Monco(url=options.mongo_url, dbName=options.db_name)
  582. break
  583. except Exception:
  584. time.sleep(1)
  585. _count += 1
  586. if _count > 20:
  587. raise
  588. # global settings stored in the db
  589. global_settings = {}
  590. BaseHandler.update_global_settings(db_connector, global_settings)
  591. init_params = dict(db=db_connector, listen_port=options.port, logger=logger,
  592. ssl_options=ssl_options, global_settings=global_settings)
  593. # If not present, we store a user 'admin' with password 'ibt2' into the database.
  594. if not db_connector.query('users', {'username': 'admin'}):
  595. db_connector.add('users',
  596. {'username': 'admin', 'password': utils.hash_password('ibt2'),
  597. 'isAdmin': True})
  598. # If present, use the cookie_secret stored into the database.
  599. cookie_secret = db_connector.get('server_settings', 'server_cookie_secret')
  600. if cookie_secret:
  601. cookie_secret = cookie_secret['value']
  602. else:
  603. # the salt guarantees its uniqueness
  604. cookie_secret = utils.hash_password('__COOKIE_SECRET__')
  605. db_connector.add('server_settings',
  606. {'_id': 'server_cookie_secret', 'value': cookie_secret})
  607. _days_path = r"/days/?(?P<day>[\d_-]+)?"
  608. _days_info_path = r"/days/(?P<day>[\d_-]+)/info"
  609. _groups_path = r"/days/(?P<day>[\d_-]+)/groups/(?P<group>.+?)"
  610. _groups_info_path = r"/days/(?P<day>[\d_-]+)/groups/(?P<group>.+?)/info"
  611. _attendees_path = r"/attendees/?(?P<id_>[\w\d_-]+)?"
  612. _current_user_path = r"/users/current/?"
  613. _users_path = r"/users/?(?P<id_>[\w\d_-]+)?/?(?P<resource>[\w\d_-]+)?/?(?P<resource_id>[\w\d_-]+)?"
  614. _settings_path = r"/settings/?(?P<id_>[\w\d_ -]+)?/?"
  615. application = tornado.web.Application([
  616. (_attendees_path, AttendeesHandler, init_params),
  617. (r'/v%s%s' % (API_VERSION, _attendees_path), AttendeesHandler, init_params),
  618. (_groups_info_path, GroupsInfoHandler, init_params),
  619. (r'/v%s%s' % (API_VERSION, _groups_info_path), GroupsInfoHandler, init_params),
  620. (_groups_path, GroupsHandler, init_params),
  621. (r'/v%s%s' % (API_VERSION, _groups_path), GroupsHandler, init_params),
  622. (_days_path, DaysHandler, init_params),
  623. (r'/v%s%s' % (API_VERSION, _days_path), DaysHandler, init_params),
  624. (_days_info_path, DaysInfoHandler, init_params),
  625. (r'/v%s%s' % (API_VERSION, _days_info_path), DaysInfoHandler, init_params),
  626. (_current_user_path, CurrentUserHandler, init_params),
  627. (r'/v%s%s' % (API_VERSION, _current_user_path), CurrentUserHandler, init_params),
  628. (_users_path, UsersHandler, init_params),
  629. (r'/v%s%s' % (API_VERSION, _users_path), UsersHandler, init_params),
  630. (_settings_path, SettingsHandler, init_params),
  631. (r'/v%s%s' % (API_VERSION, _settings_path), SettingsHandler, init_params),
  632. (r"/(?:index.html)?", RootHandler, init_params),
  633. (r'/login', LoginHandler, init_params),
  634. (r'/v%s/login' % API_VERSION, LoginHandler, init_params),
  635. (r'/logout', LogoutHandler),
  636. (r'/v%s/logout' % API_VERSION, LogoutHandler),
  637. (r'/?(.*)', tornado.web.StaticFileHandler, {"path": "dist"})
  638. ],
  639. static_path=os.path.join(os.path.dirname(__file__), "dist/static"),
  640. cookie_secret=cookie_secret,
  641. login_url='/login',
  642. debug=options.debug)
  643. http_server = tornado.httpserver.HTTPServer(application, ssl_options=ssl_options or None)
  644. logger.info('Start serving on %s://%s:%d', 'https' if ssl_options else 'http',
  645. options.address if options.address else '127.0.0.1',
  646. options.port)
  647. http_server.listen(options.port, options.address)
  648. tornado.ioloop.IOLoop.instance().start()
  649. if __name__ == '__main__':
  650. try:
  651. run()
  652. except KeyboardInterrupt:
  653. print('Stop server')