Commit 3259fc7caed421d812eaf44b7a425062fc607bc1
1 parent
5dae401a
Exists in
master
and in
1 other branch
- 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.
Showing
13 changed files
with
97 additions
and
69 deletions
Show diff stats
aprendizations/initdb.py
1 | 1 | #!/usr/bin/env python3 |
2 | 2 | |
3 | -# base | |
3 | +# python standard libraries | |
4 | 4 | import csv |
5 | 5 | import argparse |
6 | 6 | import re |
7 | 7 | from string import capwords |
8 | 8 | from concurrent.futures import ThreadPoolExecutor |
9 | 9 | |
10 | -# installed packages | |
10 | +# third party libraries | |
11 | 11 | import bcrypt |
12 | 12 | import sqlalchemy as sa |
13 | 13 | |
14 | 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 | 5 | import logging |
6 | 6 | import asyncio |
7 | 7 | |
8 | -# libraries | |
8 | +# third party libraries | |
9 | 9 | import networkx as nx |
10 | 10 | |
11 | -# this project | |
12 | -# import questions | |
13 | - | |
14 | 11 | # setup logger for this module |
15 | 12 | logger = logging.getLogger(__name__) |
16 | 13 | ... | ... |
aprendizations/learnapp.py
... | ... | @@ -5,41 +5,25 @@ import logging |
5 | 5 | from contextlib import contextmanager # `with` statement in db sessions |
6 | 6 | import asyncio |
7 | 7 | from datetime import datetime |
8 | +from random import random | |
8 | 9 | |
9 | -# user installed libraries | |
10 | +# third party libraries | |
10 | 11 | import bcrypt |
11 | 12 | from sqlalchemy import create_engine, func |
12 | 13 | from sqlalchemy.orm import sessionmaker |
13 | 14 | import networkx as nx |
14 | 15 | |
15 | 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 | 22 | # setup logger for this module |
22 | 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 | 27 | class LearnException(Exception): |
44 | 28 | pass |
45 | 29 | |
... | ... | @@ -86,38 +70,74 @@ class LearnApp(object): |
86 | 70 | |
87 | 71 | # ------------------------------------------------------------------------ |
88 | 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 | 114 | # login |
104 | 115 | # ------------------------------------------------------------------------ |
105 | - async def login(self, uid, try_pw): | |
116 | + async def login(self, uid, pw): | |
106 | 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 | 137 | if pw_ok: |
118 | 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 | 141 | else: |
122 | 142 | logger.info(f'User "{uid}" logged in') |
123 | 143 | counter = 0 |
... | ... | @@ -158,7 +178,9 @@ class LearnApp(object): |
158 | 178 | if not pw: |
159 | 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 | 185 | with self.db_session() as s: |
164 | 186 | u = s.query(Student).get(uid) | ... | ... |
aprendizations/models.py
aprendizations/questions.py
aprendizations/redirect.py
aprendizations/serve.py
... | ... | @@ -14,16 +14,16 @@ import functools |
14 | 14 | import ssl |
15 | 15 | import asyncio |
16 | 16 | |
17 | -# user installed libraries | |
17 | +# third party libraries | |
18 | 18 | import tornado.ioloop |
19 | 19 | import tornado.web |
20 | 20 | import tornado.httpserver |
21 | 21 | from tornado.escape import to_unicode |
22 | 22 | |
23 | 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 | 97 | uid = self.current_user |
98 | 98 | rankings = self.learn.get_rankings(uid) |
99 | 99 | self.render('rankings.html', |
100 | + appname=APP_NAME, | |
100 | 101 | uid=uid, |
101 | 102 | name=self.learn.get_student_name(uid), |
102 | 103 | rankings=rankings) |
... | ... | @@ -107,7 +108,9 @@ class RankingsHandler(BaseHandler): |
107 | 108 | # ---------------------------------------------------------------------------- |
108 | 109 | class LoginHandler(BaseHandler): |
109 | 110 | def get(self): |
110 | - self.render('login.html', error='') | |
111 | + self.render('login.html', | |
112 | + appname=APP_NAME, | |
113 | + error='') | |
111 | 114 | |
112 | 115 | async def post(self): |
113 | 116 | uid = self.get_body_argument('uid').lstrip('l') |
... | ... | @@ -121,7 +124,9 @@ class LoginHandler(BaseHandler): |
121 | 124 | self.set_secure_cookie('counter', counter) |
122 | 125 | self.redirect('/') |
123 | 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 | 173 | def get(self): |
169 | 174 | uid = self.current_user |
170 | 175 | self.render('maintopics-table.html', |
176 | + appname=APP_NAME, | |
171 | 177 | uid=uid, |
172 | 178 | name=self.learn.get_student_name(uid), |
173 | 179 | state=self.learn.get_student_state(uid), |
... | ... | @@ -193,6 +199,7 @@ class TopicHandler(BaseHandler): |
193 | 199 | self.redirect('/') |
194 | 200 | else: |
195 | 201 | self.render('topic.html', |
202 | + appname=APP_NAME, | |
196 | 203 | uid=uid, |
197 | 204 | name=self.learn.get_student_name(uid), |
198 | 205 | ) |
... | ... | @@ -201,9 +208,6 @@ class TopicHandler(BaseHandler): |
201 | 208 | # ---------------------------------------------------------------------------- |
202 | 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 | 211 | class FileHandler(BaseHandler): |
208 | 212 | SUPPORTED_METHODS = ['GET'] |
209 | 213 | ... | ... |
aprendizations/templates/login.html
aprendizations/templates/maintopics-table.html
aprendizations/templates/rankings.html
aprendizations/templates/topic.html
aprendizations/tools.py