##// END OF EJS Templates
Move posting validation to class-based validators for PoW and Time-based
neko259 -
r2065:770356bf default
parent child Browse files
Show More
@@ -0,0 +1,131 b''
1 import time
2 import hashlib
3
4 from django.core.cache import cache
5 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
6
7 from boards import settings as board_settings
8 from boards.settings import SECTION_FORMS
9
10
11 LAST_POST_TIME = 'last_post_time'
12 LAST_LOGIN_TIME = 'last_login_time'
13
14 POW_HASH_LENGTH = 16
15 POW_LIFE_MINUTES = 5
16
17 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
18 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
19 ERROR_INVALID_POW = 'Invalid PoW.'
20
21
22 class Validator:
23 def __init__(self, session):
24 self.errors = []
25 self.session = session
26
27 def validate(self) -> None:
28 """
29 Runs validation. If something fails, should add an error to the list.
30 """
31 pass
32
33 def get_errors(self):
34 return self.errors
35
36 def get_session(self):
37 return self.session
38
39 def add_error(self, error):
40 self.errors.append(error)
41
42
43 class TimeValidator(Validator):
44 """
45 Validates the posting speed. New post must be sent no less than after some
46 specific amount of time defined in settings.
47 """
48 def validate(self):
49 can_post = True
50
51 posting_delay = board_settings.get_int(SECTION_FORMS, 'PostingDelay')
52
53 if board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed'):
54 now = time.time()
55
56 current_delay = 0
57
58 if LAST_POST_TIME not in self.get_session():
59 self.get_session()[LAST_POST_TIME] = now
60
61 need_delay = True
62 else:
63 last_post_time = self._get_last_post_time()
64 current_delay = int(now - last_post_time)
65
66 need_delay = current_delay < posting_delay
67
68 self._set_session_cache(LAST_POST_TIME, now)
69
70 if need_delay:
71 delay = posting_delay - current_delay
72 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
73 delay) % {'delay': delay}
74 self.add_error(error_message)
75
76 can_post = False
77
78 if can_post:
79 self.get_session()[LAST_POST_TIME] = now
80 else:
81 # Reset the time since posting failed
82 self._set_session_cache(LAST_POST_TIME, self.get_session()[LAST_POST_TIME])
83
84 def _get_cache_key(self, key):
85 return '{}_{}'.format(self.get_session().session_key, key)
86
87 def _set_session_cache(self, key, value):
88 cache.set(self._get_cache_key(key), value)
89
90 def _get_session_cache(self, key):
91 return cache.get(self._get_cache_key(key))
92
93 def _get_last_post_time(self):
94 last = self._get_session_cache(LAST_POST_TIME)
95 if last is None:
96 last = self.get_session().get(LAST_POST_TIME)
97 return last
98
99
100 class PowValidator(Validator):
101 """
102 Validates prof of work. A hash is computed on the client side and validated
103 here, all data must match. PoW difficulty is set in the settings.
104 """
105 def __init__(self, session, timestamp, iteration, guess, text):
106 super().__init__(session)
107
108 self.timestamp = timestamp
109 self.iteration = iteration
110 self.guess = guess
111
112 self.text = text
113
114 def validate(self):
115 if self.timestamp and self.iteration and self.guess:
116 self._validate_hash(self.timestamp, self.iteration,
117 self.guess, self.text)
118 else:
119 self.add_error(_(ERROR_INVALID_POW))
120
121 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
122 payload = timestamp + message.replace('\r\n', '\n')
123 difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
124 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
125 if len(target) < POW_HASH_LENGTH:
126 target = '0' * (POW_HASH_LENGTH - len(target)) + target
127
128 computed_guess = hashlib.sha256((payload + iteration).encode()) \
129 .hexdigest()[0:POW_HASH_LENGTH]
130 if guess != computed_guess or guess > target:
131 self.add_error(_('Invalid PoW.'))
@@ -19,6 +19,7 b" SETTING_IMAGE_VIEWER = 'image_viewer'"
19 SETTING_IMAGES = 'images_aliases'
19 SETTING_IMAGES = 'images_aliases'
20 SETTING_ONLY_FAVORITES = 'only_favorites'
20 SETTING_ONLY_FAVORITES = 'only_favorites'
21 SETTING_LAST_POSTS = 'last_posts'
21 SETTING_LAST_POSTS = 'last_posts'
22 SETTING_CONFIRMED_USER = 'confirmed_user'
22
23
23 DEFAULT_THEME = 'md'
24 DEFAULT_THEME = 'md'
24
25
@@ -1,12 +1,9 b''
1 import logging
1 import logging
2 import time
3
2
4 import hashlib
5 import pytz
3 import pytz
6 import re
4 import re
7 from PIL import Image
5 from PIL import Image
8 from django import forms
6 from django import forms
9 from django.core.cache import cache
10 from django.core.files.images import get_image_dimensions
7 from django.core.files.images import get_image_dimensions
11 from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
8 from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
12 from django.forms.utils import ErrorList
9 from django.forms.utils import ErrorList
@@ -15,9 +12,11 b' from django.utils.translation import uge'
15 import boards.settings as board_settings
12 import boards.settings as board_settings
16 from boards import utils
13 from boards import utils
17 from boards.abstracts.constants import REGEX_TAGS
14 from boards.abstracts.constants import REGEX_TAGS
18 from boards.abstracts.settingsmanager import get_settings_manager
15 from boards.abstracts.settingsmanager import get_settings_manager, \
16 SETTING_CONFIRMED_USER
19 from boards.abstracts.sticker_factory import get_attachment_by_alias
17 from boards.abstracts.sticker_factory import get_attachment_by_alias
20 from boards.forms.fields import UrlFileField
18 from boards.forms.fields import UrlFileField
19 from boards.forms.validators import TimeValidator, PowValidator
21 from boards.mdx_neboard import formatters
20 from boards.mdx_neboard import formatters
22 from boards.models import Attachment
21 from boards.models import Attachment
23 from boards.models import Tag
22 from boards.models import Tag
@@ -25,15 +24,13 b' from boards.models.attachment import Sti'
25 from boards.models.attachment.downloaders import download, REGEX_MAGNET
24 from boards.models.attachment.downloaders import download, REGEX_MAGNET
26 from boards.models.attachment.viewers import FILE_TYPES_IMAGE
25 from boards.models.attachment.viewers import FILE_TYPES_IMAGE
27 from boards.models.post import TITLE_MAX_LENGTH
26 from boards.models.post import TITLE_MAX_LENGTH
27 from boards.settings import SECTION_FORMS
28 from boards.utils import validate_file_size, get_file_mimetype, \
28 from boards.utils import validate_file_size, get_file_mimetype, \
29 FILE_EXTENSION_DELIMITER, get_tripcode_from_text
29 FILE_EXTENSION_DELIMITER, get_tripcode_from_text
30 from boards.settings import SECTION_FORMS
31
30
32 FORMAT_PANEL_BUTTON = '<span class="mark_btn" ' \
31 FORMAT_PANEL_BUTTON = '<span class="mark_btn" ' \
33 'onClick="addMarkToMsg(\'{}\', \'{}\')">{}{}{}</span>'
32 'onClick="addMarkToMsg(\'{}\', \'{}\')">{}{}{}</span>'
34
33
35 POW_HASH_LENGTH = 16
36 POW_LIFE_MINUTES = 5
37
34
38 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
35 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
39 REGEX_URL = re.compile(r'^(http|https|ftp):\/\/', re.UNICODE)
36 REGEX_URL = re.compile(r'^(http|https|ftp):\/\/', re.UNICODE)
@@ -43,8 +40,6 b' VETERAN_POSTING_DELAY = 5'
43 ATTRIBUTE_PLACEHOLDER = 'placeholder'
40 ATTRIBUTE_PLACEHOLDER = 'placeholder'
44 ATTRIBUTE_ROWS = 'rows'
41 ATTRIBUTE_ROWS = 'rows'
45
42
46 LAST_POST_TIME = 'last_post_time'
47 LAST_LOGIN_TIME = 'last_login_time'
48 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
43 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
49 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
44 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
50
45
@@ -56,8 +51,6 b" LABEL_FILE = _('File')"
56 LABEL_DUPLICATES = _('Check for duplicates')
51 LABEL_DUPLICATES = _('Check for duplicates')
57 LABEL_URL = _('Do not download URLs')
52 LABEL_URL = _('Do not download URLs')
58
53
59 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
60 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
61 ERROR_MANY_FILES = 'You can post no more than %(files)d file.'
54 ERROR_MANY_FILES = 'You can post no more than %(files)d file.'
62 ERROR_MANY_FILES_PLURAL = 'You can post no more than %(files)d files.'
55 ERROR_MANY_FILES_PLURAL = 'You can post no more than %(files)d files.'
63 ERROR_DUPLICATES = 'Some files are already present on the board.'
56 ERROR_DUPLICATES = 'Some files are already present on the board.'
@@ -249,18 +242,21 b' class PostForm(NeboardForm):'
249 limit_first = board_settings.get_bool(SECTION_FORMS, 'LimitFirstPosting')
242 limit_first = board_settings.get_bool(SECTION_FORMS, 'LimitFirstPosting')
250
243
251 settings_manager = get_settings_manager(self)
244 settings_manager = get_settings_manager(self)
252 if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting('confirmed_user')):
245 if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting(SETTING_CONFIRMED_USER)):
253 pow_difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
246 pow_difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
254 if pow_difficulty > 0:
247 if pow_difficulty > 0:
255 # PoW-based
248 validator = PowValidator(
256 if cleaned_data['timestamp'] \
249 self.session, cleaned_data['timestamp'],
257 and cleaned_data['iteration'] and cleaned_data['guess'] \
250 cleaned_data['iteration'], cleaned_data['guess'],
258 and not settings_manager.get_setting('confirmed_user'):
251 cleaned_data['text'])
259 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
260 else:
252 else:
261 # Time-based
253 validator = TimeValidator(self.session)
262 self._validate_posting_speed()
254
263 settings_manager.set_setting('confirmed_user', True)
255 validator.validate()
256 for error in validator.get_errors():
257 self._add_general_error(error)
258
259 settings_manager.set_setting(SETTING_CONFIRMED_USER, True)
264 if self.cleaned_data['download_mode'] == DOWN_MODE_DOWNLOAD_UNIQUE:
260 if self.cleaned_data['download_mode'] == DOWN_MODE_DOWNLOAD_UNIQUE:
265 self._check_file_duplicates(self.get_files())
261 self._check_file_duplicates(self.get_files())
266
262
@@ -399,57 +395,6 b' class PostForm(NeboardForm):'
399 error_message = _('Either text or file must be entered.')
395 error_message = _('Either text or file must be entered.')
400 self._add_general_error(error_message)
396 self._add_general_error(error_message)
401
397
402 def _get_cache_key(self, key):
403 return '{}_{}'.format(self.session.session_key, key)
404
405 def _set_session_cache(self, key, value):
406 cache.set(self._get_cache_key(key), value)
407
408 def _get_session_cache(self, key):
409 return cache.get(self._get_cache_key(key))
410
411 def _get_last_post_time(self):
412 last = self._get_session_cache(LAST_POST_TIME)
413 if last is None:
414 last = self.session.get(LAST_POST_TIME)
415 return last
416
417 def _validate_posting_speed(self):
418 can_post = True
419
420 posting_delay = board_settings.get_int(SECTION_FORMS, 'PostingDelay')
421
422 if board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed'):
423 now = time.time()
424
425 current_delay = 0
426
427 if LAST_POST_TIME not in self.session:
428 self.session[LAST_POST_TIME] = now
429
430 need_delay = True
431 else:
432 last_post_time = self._get_last_post_time()
433 current_delay = int(now - last_post_time)
434
435 need_delay = current_delay < posting_delay
436
437 self._set_session_cache(LAST_POST_TIME, now)
438
439 if need_delay:
440 delay = posting_delay - current_delay
441 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
442 delay) % {'delay': delay}
443 self._add_general_error(error_message)
444
445 can_post = False
446
447 if can_post:
448 self.session[LAST_POST_TIME] = now
449 else:
450 # Reset the time since posting failed
451 self._set_session_cache(LAST_POST_TIME, self.session[LAST_POST_TIME])
452
453 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
398 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
454 """
399 """
455 Gets an file file from URL.
400 Gets an file file from URL.
@@ -462,18 +407,6 b' class PostForm(NeboardForm):'
462 except Exception as e:
407 except Exception as e:
463 raise forms.ValidationError(e)
408 raise forms.ValidationError(e)
464
409
465 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
466 payload = timestamp + message.replace('\r\n', '\n')
467 difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
468 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
469 if len(target) < POW_HASH_LENGTH:
470 target = '0' * (POW_HASH_LENGTH - len(target)) + target
471
472 computed_guess = hashlib.sha256((payload + iteration).encode())\
473 .hexdigest()[0:POW_HASH_LENGTH]
474 if guess != computed_guess or guess > target:
475 self._add_general_error(_('Invalid PoW.'))
476
477 def _check_file_duplicates(self, files):
410 def _check_file_duplicates(self, files):
478 for file in files:
411 for file in files:
479 file_hash = utils.get_file_hash(file)
412 file_hash = utils.get_file_hash(file)
General Comments 0
You need to be logged in to leave comments. Login now