Commit 3259fc7caed421d812eaf44b7a425062fc607bc1

Authored by Miguel Barão
1 parent 5dae401a
Exists in master and in 1 other branch dev

- Modified login to prevent timing attacks.

- Sanity checks now support tests_right and tests_wrong in questions
(unit testing for questions)
- Application name is defined in __init__.py only, so that changing
name becomes easier.
aprendizations/initdb.py
1 #!/usr/bin/env python3 1 #!/usr/bin/env python3
2 2
3 -# base 3 +# python standard libraries
4 import csv 4 import csv
5 import argparse 5 import argparse
6 import re 6 import re
7 from string import capwords 7 from string import capwords
8 from concurrent.futures import ThreadPoolExecutor 8 from concurrent.futures import ThreadPoolExecutor
9 9
10 -# installed packages 10 +# third party libraries
11 import bcrypt 11 import bcrypt
12 import sqlalchemy as sa 12 import sqlalchemy as sa
13 13
14 # this project 14 # this project
15 -from aprendizations.models import Base, Student 15 +from .models import Base, Student
16 16
17 17
18 # =========================================================================== 18 # ===========================================================================
aprendizations/knowledge.py
@@ -5,12 +5,9 @@ from datetime import datetime @@ -5,12 +5,9 @@ from datetime import datetime
5 import logging 5 import logging
6 import asyncio 6 import asyncio
7 7
8 -# libraries 8 +# third party libraries
9 import networkx as nx 9 import networkx as nx
10 10
11 -# this project  
12 -# import questions  
13 -  
14 # setup logger for this module 11 # setup logger for this module
15 logger = logging.getLogger(__name__) 12 logger = logging.getLogger(__name__)
16 13
aprendizations/learnapp.py
@@ -5,41 +5,25 @@ import logging @@ -5,41 +5,25 @@ import logging
5 from contextlib import contextmanager # `with` statement in db sessions 5 from contextlib import contextmanager # `with` statement in db sessions
6 import asyncio 6 import asyncio
7 from datetime import datetime 7 from datetime import datetime
  8 +from random import random
8 9
9 -# user installed libraries 10 +# third party libraries
10 import bcrypt 11 import bcrypt
11 from sqlalchemy import create_engine, func 12 from sqlalchemy import create_engine, func
12 from sqlalchemy.orm import sessionmaker 13 from sqlalchemy.orm import sessionmaker
13 import networkx as nx 14 import networkx as nx
14 15
15 # this project 16 # this project
16 -from aprendizations.models import Student, Answer, Topic, StudentTopic  
17 -from aprendizations.knowledge import StudentKnowledge  
18 -from aprendizations.questions import QFactory  
19 -from aprendizations.tools import load_yaml 17 +from .models import Student, Answer, Topic, StudentTopic
  18 +from .knowledge import StudentKnowledge
  19 +from .questions import QFactory
  20 +from .tools import load_yaml
20 21
21 # setup logger for this module 22 # setup logger for this module
22 logger = logging.getLogger(__name__) 23 logger = logging.getLogger(__name__)
23 24
24 25
25 # ============================================================================ 26 # ============================================================================
26 -# helper functions  
27 -# ============================================================================  
28 -async def _bcrypt_hash(a, b):  
29 - loop = asyncio.get_running_loop()  
30 - return await loop.run_in_executor(None, bcrypt.hashpw,  
31 - a.encode('utf-8'), b)  
32 -  
33 -  
34 -async def check_password(try_pw: str, pw: str) -> bool:  
35 - return pw == await _bcrypt_hash(try_pw, pw)  
36 -  
37 -  
38 -async def bcrypt_hash_gen(new_pw: str):  
39 - return await _bcrypt_hash(new_pw, bcrypt.gensalt())  
40 -  
41 -  
42 -# ============================================================================  
43 class LearnException(Exception): 27 class LearnException(Exception):
44 pass 28 pass
45 29
@@ -86,38 +70,74 @@ class LearnApp(object): @@ -86,38 +70,74 @@ class LearnApp(object):
86 70
87 # ------------------------------------------------------------------------ 71 # ------------------------------------------------------------------------
88 def sanity_check_questions(self): 72 def sanity_check_questions(self):
89 - def check_question(qref, q):  
90 - logger.debug(f'Generating {qref}...')  
91 - try:  
92 - q.generate()  
93 - except Exception as e:  
94 - logger.error(f'Sanity check failed in "{qref}"')  
95 - raise e 73 + logger.info('Starting sanity checks (may take a while...)')
96 74
97 - logger.info('Starting sanity checks...')  
98 - for qref, q in self.factory.items():  
99 - check_question(qref, q)  
100 - logger.info('Sanity checks passed.') 75 + errors = 0
  76 + for qref in self.factory:
  77 + logger.debug(f'Checking "{qref}"...')
  78 + q = self.factory[qref].generate()
  79 + try:
  80 + q = self.factory[qref].generate()
  81 + except Exception:
  82 + logger.error(f'Failed to generate "{qref}".')
  83 + errors += 1
  84 + raise
  85 + continue
  86 +
  87 + if 'tests_right' in q:
  88 + for t in q['tests_right']:
  89 + q['answer'] = t
  90 + q.correct()
  91 + if q['grade'] < 1.0:
  92 + logger.error(f'Failed to correct right answer in '
  93 + f'"{qref}".')
  94 + errors += 1
  95 + continue # to next right test
  96 +
  97 + if 'tests_wrong' in q:
  98 + for t in q['tests_wrong']:
  99 + q['answer'] = t
  100 + q.correct()
  101 + if q['grade'] >= 1.0:
  102 + logger.error(f'Failed to correct right answer in '
  103 + f'"{qref}".')
  104 + errors += 1
  105 + continue # to next wrong test
  106 +
  107 + if errors > 0:
  108 + logger.info(f'{errors:>6} errors found.')
  109 + raise
  110 + else:
  111 + logger.info('No errors found.')
101 112
102 # ------------------------------------------------------------------------ 113 # ------------------------------------------------------------------------
103 # login 114 # login
104 # ------------------------------------------------------------------------ 115 # ------------------------------------------------------------------------
105 - async def login(self, uid, try_pw): 116 + async def login(self, uid, pw):
106 with self.db_session() as s: 117 with self.db_session() as s:
107 - try:  
108 - name, password = s.query(Student.name, Student.password) \  
109 - .filter_by(id=uid) \  
110 - .one()  
111 - except Exception:  
112 - logger.info(f'User "{uid}" does not exist')  
113 - return False 118 + found = s.query(Student.name, Student.password) \
  119 + .filter_by(id=uid) \
  120 + .one_or_none()
  121 +
  122 + # wait random time to minimize timing attacks
  123 + await asyncio.sleep(random())
  124 +
  125 + loop = asyncio.get_running_loop()
  126 + if found is None:
  127 + logger.info(f'User "{uid}" does not exist')
  128 + await loop.run_in_executor(None, bcrypt.hashpw, b'',
  129 + bcrypt.gensalt()) # just spend time
  130 + return False
114 131
115 - pw_ok = await check_password(try_pw, password) # async bcrypt 132 + else:
  133 + name, hashed_pw = found
  134 + pw_ok = await loop.run_in_executor(None, bcrypt.checkpw,
  135 + pw.encode('utf-8'), hashed_pw)
116 136
117 if pw_ok: 137 if pw_ok:
118 if uid in self.online: 138 if uid in self.online:
119 - logger.warning(f'User "{uid}" already logged in, overwriting')  
120 - counter = self.online[uid]['counter'] # simultaneous logins 139 + logger.warning(f'User "{uid}" already logged in')
  140 + counter = self.online[uid]['counter']
121 else: 141 else:
122 logger.info(f'User "{uid}" logged in') 142 logger.info(f'User "{uid}" logged in')
123 counter = 0 143 counter = 0
@@ -158,7 +178,9 @@ class LearnApp(object): @@ -158,7 +178,9 @@ class LearnApp(object):
158 if not pw: 178 if not pw:
159 return False 179 return False
160 180
161 - pw = await bcrypt_hash_gen(pw) 181 + loop = asyncio.get_running_loop()
  182 + pw = await loop.run_in_executor(None, bcrypt.hashpw,
  183 + pw.encode('utf-8'), bcrypt.gensalt())
162 184
163 with self.db_session() as s: 185 with self.db_session() as s:
164 u = s.query(Student).get(uid) 186 u = s.query(Student).get(uid)
aprendizations/models.py
1 1
  2 +# third party libraries
2 from sqlalchemy import Column, ForeignKey, Integer, Float, String 3 from sqlalchemy import Column, ForeignKey, Integer, Float, String
3 from sqlalchemy.ext.declarative import declarative_base 4 from sqlalchemy.ext.declarative import declarative_base
4 from sqlalchemy.orm import relationship 5 from sqlalchemy.orm import relationship
aprendizations/questions.py
@@ -7,7 +7,7 @@ import logging @@ -7,7 +7,7 @@ import logging
7 import asyncio 7 import asyncio
8 8
9 # this project 9 # this project
10 -from aprendizations.tools import run_script 10 +from .tools import run_script
11 11
12 # setup logger for this module 12 # setup logger for this module
13 logger = logging.getLogger(__name__) 13 logger = logging.getLogger(__name__)
aprendizations/redirect.py
1 #!/usr/bin/env python3 1 #!/usr/bin/env python3
2 2
  3 +# python standard libraries
  4 +import argparse
  5 +
  6 +# third party libraries
3 from tornado.web import RedirectHandler, Application 7 from tornado.web import RedirectHandler, Application
4 from tornado.ioloop import IOLoop 8 from tornado.ioloop import IOLoop
5 -import argparse  
6 9
7 10
8 def main(): 11 def main():
aprendizations/serve.py
@@ -14,16 +14,16 @@ import functools @@ -14,16 +14,16 @@ import functools
14 import ssl 14 import ssl
15 import asyncio 15 import asyncio
16 16
17 -# user installed libraries 17 +# third party libraries
18 import tornado.ioloop 18 import tornado.ioloop
19 import tornado.web 19 import tornado.web
20 import tornado.httpserver 20 import tornado.httpserver
21 from tornado.escape import to_unicode 21 from tornado.escape import to_unicode
22 22
23 # this project 23 # this project
24 -from aprendizations.learnapp import LearnApp  
25 -from aprendizations.tools import load_yaml, md_to_html  
26 -from aprendizations import APP_NAME 24 +from .learnapp import LearnApp
  25 +from .tools import load_yaml, md_to_html
  26 +from . import APP_NAME
27 27
28 28
29 # ---------------------------------------------------------------------------- 29 # ----------------------------------------------------------------------------
@@ -97,6 +97,7 @@ class RankingsHandler(BaseHandler): @@ -97,6 +97,7 @@ class RankingsHandler(BaseHandler):
97 uid = self.current_user 97 uid = self.current_user
98 rankings = self.learn.get_rankings(uid) 98 rankings = self.learn.get_rankings(uid)
99 self.render('rankings.html', 99 self.render('rankings.html',
  100 + appname=APP_NAME,
100 uid=uid, 101 uid=uid,
101 name=self.learn.get_student_name(uid), 102 name=self.learn.get_student_name(uid),
102 rankings=rankings) 103 rankings=rankings)
@@ -107,7 +108,9 @@ class RankingsHandler(BaseHandler): @@ -107,7 +108,9 @@ class RankingsHandler(BaseHandler):
107 # ---------------------------------------------------------------------------- 108 # ----------------------------------------------------------------------------
108 class LoginHandler(BaseHandler): 109 class LoginHandler(BaseHandler):
109 def get(self): 110 def get(self):
110 - self.render('login.html', error='') 111 + self.render('login.html',
  112 + appname=APP_NAME,
  113 + error='')
111 114
112 async def post(self): 115 async def post(self):
113 uid = self.get_body_argument('uid').lstrip('l') 116 uid = self.get_body_argument('uid').lstrip('l')
@@ -121,7 +124,9 @@ class LoginHandler(BaseHandler): @@ -121,7 +124,9 @@ class LoginHandler(BaseHandler):
121 self.set_secure_cookie('counter', counter) 124 self.set_secure_cookie('counter', counter)
122 self.redirect('/') 125 self.redirect('/')
123 else: 126 else:
124 - self.render('login.html', error='Número ou senha incorrectos') 127 + self.render('login.html',
  128 + appname=APP_NAME,
  129 + error='Número ou senha incorrectos')
125 130
126 131
127 # ---------------------------------------------------------------------------- 132 # ----------------------------------------------------------------------------
@@ -168,6 +173,7 @@ class RootHandler(BaseHandler): @@ -168,6 +173,7 @@ class RootHandler(BaseHandler):
168 def get(self): 173 def get(self):
169 uid = self.current_user 174 uid = self.current_user
170 self.render('maintopics-table.html', 175 self.render('maintopics-table.html',
  176 + appname=APP_NAME,
171 uid=uid, 177 uid=uid,
172 name=self.learn.get_student_name(uid), 178 name=self.learn.get_student_name(uid),
173 state=self.learn.get_student_state(uid), 179 state=self.learn.get_student_state(uid),
@@ -193,6 +199,7 @@ class TopicHandler(BaseHandler): @@ -193,6 +199,7 @@ class TopicHandler(BaseHandler):
193 self.redirect('/') 199 self.redirect('/')
194 else: 200 else:
195 self.render('topic.html', 201 self.render('topic.html',
  202 + appname=APP_NAME,
196 uid=uid, 203 uid=uid,
197 name=self.learn.get_student_name(uid), 204 name=self.learn.get_student_name(uid),
198 ) 205 )
@@ -201,9 +208,6 @@ class TopicHandler(BaseHandler): @@ -201,9 +208,6 @@ class TopicHandler(BaseHandler):
201 # ---------------------------------------------------------------------------- 208 # ----------------------------------------------------------------------------
202 # Serves files from the /public subdir of the topics. 209 # Serves files from the /public subdir of the topics.
203 # ---------------------------------------------------------------------------- 210 # ----------------------------------------------------------------------------
204 -  
205 -# FIXME error in many situations... images are not shown...  
206 -# seems to happen when the browser sends two GET requests at the same time  
207 class FileHandler(BaseHandler): 211 class FileHandler(BaseHandler):
208 SUPPORTED_METHODS = ['GET'] 212 SUPPORTED_METHODS = ['GET']
209 213
aprendizations/templates/login.html
1 <!DOCTYPE html> 1 <!DOCTYPE html>
2 <html lang="en"> 2 <html lang="en">
3 <head> 3 <head>
4 - <title>iLearn</title> 4 + <title>{{appname}}</title>
5 <link rel="icon" href="/static/favicon.ico"> 5 <link rel="icon" href="/static/favicon.ico">
6 6
7 <meta charset="utf-8"> 7 <meta charset="utf-8">
aprendizations/templates/maintopics-table.html
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 <!doctype html> 3 <!doctype html>
4 <html lang="pt-PT"> 4 <html lang="pt-PT">
5 <head> 5 <head>
6 - <title>iLearn</title> 6 + <title>{{appname}}</title>
7 <link rel="icon" href="/static/favicon.ico"> 7 <link rel="icon" href="/static/favicon.ico">
8 8
9 <meta charset="utf-8"> 9 <meta charset="utf-8">
aprendizations/templates/rankings.html
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 <!doctype html> 3 <!doctype html>
4 <html lang="pt-PT"> 4 <html lang="pt-PT">
5 <head> 5 <head>
6 - <title>iLearn</title> 6 + <title>{{appname}}</title>
7 <link rel="icon" href="/static/favicon.ico"> 7 <link rel="icon" href="/static/favicon.ico">
8 8
9 <meta charset="utf-8"> 9 <meta charset="utf-8">
aprendizations/templates/topic.html
1 <!DOCTYPE html> 1 <!DOCTYPE html>
2 <html> 2 <html>
3 <head> 3 <head>
4 - <title>iLearn</title> 4 + <title>{{appname}}</title>
5 <link rel="icon" href="/static/favicon.ico"> 5 <link rel="icon" href="/static/favicon.ico">
6 6
7 <meta charset="utf-8"> 7 <meta charset="utf-8">
aprendizations/tools.py
@@ -5,7 +5,7 @@ import subprocess @@ -5,7 +5,7 @@ import subprocess
5 import logging 5 import logging
6 import re 6 import re
7 7
8 -# user installed libraries 8 +# third party libraries
9 import yaml 9 import yaml
10 import mistune 10 import mistune
11 from pygments import highlight 11 from pygments import highlight
@@ -6,5 +6,6 @@ @@ -6,5 +6,6 @@
6 "codemirror": "^5.45.0", 6 "codemirror": "^5.45.0",
7 "mathjax": "^2.7.5", 7 "mathjax": "^2.7.5",
8 "mdbootstrap": "^4.7.6" 8 "mdbootstrap": "^4.7.6"
9 - } 9 + },
  10 + "private": true
10 } 11 }