#!/usr/bin/env python3.6 # python standard library import os import sys import json import base64 import uuid import concurrent.futures import logging.config import argparse # user installed libraries try: import markdown import tornado.ioloop import tornado.web import tornado.httpserver from tornado import template, gen except ImportError: print('Some python packages are missing. See README.md for instructions.') sys.exit(1) # this project from app import LearnApp from tools import load_yaml, md # A thread pool to be used for password hashing with bcrypt. FIXME and other things? # executor = concurrent.futures.ThreadPoolExecutor(2) # ============================================================================ # WebApplication - Tornado Web Server # ============================================================================ class WebApplication(tornado.web.Application): def __init__(self, learnapp): handlers = [ (r'/login', LoginHandler), (r'/logout', LogoutHandler), (r'/change_password', ChangePasswordHandler), (r'/question', QuestionHandler), (r'/', LearnHandler), (r'/(.+)', FileHandler), ] settings = { 'template_path': os.path.join(os.path.dirname(__file__), 'templates'), 'static_path': os.path.join(os.path.dirname(__file__), 'static'), 'static_url_prefix': '/static/', # this is the default 'xsrf_cookies': False, # FIXME see how to do it... 'cookie_secret': base64.b64encode(uuid.uuid4().bytes), 'login_url': '/login', 'debug': True, } super().__init__(handlers, **settings) self.learn = learnapp # ============================================================================ # Handlers # ============================================================================ # ---------------------------------------------------------------------------- # Base handler common to all handlers. # ---------------------------------------------------------------------------- class BaseHandler(tornado.web.RequestHandler): @property def learn(self): return self.application.learn def get_current_user(self): cookie = self.get_secure_cookie("user") if cookie: user = cookie.decode('utf-8') # FIXME if the cookie exists but user is not in learn.online, this will force new login and store new (duplicate?) cookie. is this correct?? if user in self.learn.online: return user # ---------------------------------------------------------------------------- # /auth/login and /auth/logout # ---------------------------------------------------------------------------- class LoginHandler(BaseHandler): def get(self): self.render('login.html', error='') # @gen.coroutine def post(self): uid = self.get_body_argument('uid') pw = self.get_body_argument('pw') if self.learn.login(uid, pw): self.set_secure_cookie("user", str(uid), expires_days=30) self.redirect(self.get_argument("next", "/")) else: self.render("login.html", error='Número ou senha incorrectos') # ---------------------------------------------------------------------------- class LogoutHandler(BaseHandler): @tornado.web.authenticated def get(self): self.learn.logout(self.current_user) self.clear_cookie('user') self.redirect(self.get_argument('next', '/')) # ---------------------------------------------------------------------------- class ChangePasswordHandler(BaseHandler): @tornado.web.authenticated def post(self): uid = self.current_user pw = self.get_body_arguments('new_password')[0]; if self.learn.change_password(uid, pw): notification = tornado.escape.to_unicode( self.render_string( 'notification.html', type='success', msg='A password foi alterada!' ) ) else: notification = tornado.escape.to_unicode(self.render_string('notification.html', type='danger', msg='A password não foi alterada!')) self.write({'msg': notification}) # ---------------------------------------------------------------------------- # /learn # ---------------------------------------------------------------------------- class LearnHandler(BaseHandler): @tornado.web.authenticated def get(self): uid = self.current_user self.render('learn.html', uid=uid, name=self.learn.get_student_name(uid), title=self.learn.get_title(), ) # ---------------------------------------------------------------------------- class FileHandler(BaseHandler): @tornado.web.authenticated def get(self, filename): uid = self.current_user public_dir = self.learn.get_current_public_dir(uid) try: with open(os.path.join(public_dir, filename), 'rb') as f: self.write(f.read()) except FileNotFoundError: raise tornado.web.HTTPError(404) # ---------------------------------------------------------------------------- # respond to AJAX to get a JSON question class QuestionHandler(BaseHandler): templates = { 'information': 'question-information.html', 'checkbox': 'question-checkbox.html', 'radio': 'question-radio.html', 'text': 'question-text.html', 'text_regex': 'question-text.html', 'text_numeric': 'question-text.html', 'textarea': 'question-textarea.html', } @tornado.web.authenticated def get(self): self.redirect('/') @tornado.web.authenticated def post(self): # ref = self.get_body_arguments('question_ref') user = self.current_user answer = self.get_body_arguments('answer') next_question = self.learn.check_answer(user, answer) state = self.learn.get_student_state(user) # all topics progress = self.learn.get_student_progress(user) # in the current topic if next_question is not None: question_html = self.render_string( self.templates[next_question['type']], question=next_question, # dictionary with the question md=md, # function that renders markdown to html ) topics_html = self.render_string( 'topics.html', state=state, topicname=self.learn.get_topic_name, # function that translates topic references to names ) self.write({ 'method': 'new_question', 'params': { 'question': tornado.escape.to_unicode(question_html), 'state': tornado.escape.to_unicode(topics_html), 'progress': progress, }, }) else: self.write({ 'method': 'shake', 'params': { 'progress': progress, }, }) # ---------------------------------------------------------------------------- def main(): SERVER_PATH = os.path.dirname(os.path.realpath(__file__)) LOGGER_CONF = os.path.join(SERVER_PATH, 'config/logger.yaml') # --- Commandline argument parsing argparser = argparse.ArgumentParser(description='Server for online learning. Enrolled students and topics have to be previously configured. Please read the documentation included with this software before running the server.') # FIXME: # serverconf_file = path.normpath(path.join(SERVER_PATH, 'config', 'server.conf')) # argparser.add_argument('--conf', default=serverconf_file, type=str, help='server configuration file') # argparser.add_argument('--debug', action='store_true', help='Enable debug logging.') # argparser.add_argument('--allow-all', action='store_true', # help='Students are initially allowed to login (can be denied later)') argparser.add_argument('conffile', type=str, nargs='+', help='Topics configuration file in YAML format.') # FIXME only one supported at the moment arg = argparser.parse_args() # --- Setup logging try: logging.config.dictConfig(load_yaml(LOGGER_CONF)) except: # FIXME should this be done in a different way? print('An error ocurred while setting up the logging system.') print('Common causes:\n - inexistent directory "logs"?\n - write permission to "logs" directory?') sys.exit(1) # --- start application learnapp = LearnApp(arg.conffile[0]) try: webapp = WebApplication(learnapp) except Exception as e: logging.critical('Can\'t start application.') # sys.exit(1) raise e # FIXME # --- create webserver http_server = tornado.httpserver.HTTPServer(webapp, ssl_options={ "certfile": "certs/cert.pem", "keyfile": "certs/key.pem" }) http_server.listen(8443) # --- start webserver try: logging.info('Webserver running...') tornado.ioloop.IOLoop.current().start() # running... except KeyboardInterrupt: tornado.ioloop.IOLoop.current().stop() logging.info('Webserver stopped.') # ---------------------------------------------------------------------------- if __name__ == "__main__": main()