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 | #!/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
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
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 |