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( |
|
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 |
|
|
248 | validator = PowValidator( | |
256 |
|
|
249 | self.session, cleaned_data['timestamp'], | |
257 |
|
|
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