##// END OF EJS Templates
Save signatures when the post is parsed for the later use
neko259 -
r1244:0f0731ca decentral
parent child Browse files
Show More
@@ -1,128 +1,128 b''
1 1 from datetime import datetime, timedelta, date
2 2 from datetime import time as dtime
3 3 import logging
4 4 from django.db import models, transaction
5 5 from django.utils import timezone
6 6 from boards import utils
7 7 from boards.mdx_neboard import Parser
8 8 from boards.models import PostImage
9 9 import boards.models
10 10
11 11 __author__ = 'vurdalak'
12 12
13 13
14 14 NO_IP = '0.0.0.0'
15 15 POSTS_PER_DAY_RANGE = 7
16 16
17 17
18 18 class PostManager(models.Manager):
19 19 @transaction.atomic
20 20 def create_post(self, title: str, text: str, image=None, thread=None,
21 21 ip=NO_IP, tags: list=None, opening_posts: list=None):
22 22 """
23 23 Creates new post
24 24 """
25 25
26 26 is_banned = boards.models.Ban.objects.filter(ip=ip).exists()
27 27
28 28 # TODO Raise specific exception and catch it in the views
29 29 if is_banned:
30 30 raise Exception("This user is banned")
31 31
32 32 if not tags:
33 33 tags = []
34 34 if not opening_posts:
35 35 opening_posts = []
36 36
37 37 posting_time = timezone.now()
38 38 new_thread = False
39 39 if not thread:
40 40 thread = boards.models.thread.Thread.objects.create(
41 41 bump_time=posting_time, last_edit_time=posting_time)
42 42 list(map(thread.tags.add, tags))
43 43 boards.models.thread.Thread.objects.process_oldest_threads()
44 44 new_thread = True
45 45
46 46 pre_text = Parser().preparse(text)
47 47
48 48 post = self.create(title=title,
49 49 text=pre_text,
50 50 pub_time=posting_time,
51 51 poster_ip=ip,
52 52 thread=thread,
53 53 last_edit_time=posting_time)
54 54 post.threads.add(thread)
55 55
56 56 post.set_global_id()
57 57
58 58 logger = logging.getLogger('boards.post.create')
59 59
60 60 logger.info('Created post {} by {}'.format(post, post.poster_ip))
61 61
62 62 if image:
63 63 post.images.add(PostImage.objects.create_with_hash(image))
64 64
65 65 if not new_thread:
66 66 thread.last_edit_time = posting_time
67 67 thread.bump()
68 68 thread.save()
69 69
70 70 post.build_url()
71 71 post.connect_replies()
72 72 post.connect_threads(opening_posts)
73 73 post.connect_notifications()
74 74
75 75 return post
76 76
77 77 @transaction.atomic
78 def import_post(self, title: str, text: str, pub_time: str,
78 def import_post(self, title: str, text: str, pub_time: str, global_id,
79 79 opening_post=None, tags=list()):
80 80 if opening_post is None:
81 81 thread = boards.models.thread.Thread.objects.create(
82 82 bump_time=pub_time, last_edit_time=pub_time)
83 83 list(map(thread.tags.add, tags))
84 84 else:
85 85 thread = opening_post.get_thread()
86 86
87 87 post = self.create(title=title, text=text,
88 88 pub_time=pub_time,
89 89 poster_ip=NO_IP,
90 90 last_edit_time=pub_time,
91 thread_id=thread.id)
91 thread_id=thread.id, global_id=global_id)
92 92
93 93 post.build_url()
94 94 post.connect_replies()
95 95 post.connect_notifications()
96 96
97 97 return post
98 98
99 99 def delete_posts_by_ip(self, ip):
100 100 """
101 101 Deletes all posts of the author with same IP
102 102 """
103 103
104 104 posts = self.filter(poster_ip=ip)
105 105 for post in posts:
106 106 post.delete()
107 107
108 108 @utils.cached_result()
109 109 def get_posts_per_day(self) -> float:
110 110 """
111 111 Gets average count of posts per day for the last 7 days
112 112 """
113 113
114 114 day_end = date.today()
115 115 day_start = day_end - timedelta(POSTS_PER_DAY_RANGE)
116 116
117 117 day_time_start = timezone.make_aware(datetime.combine(
118 118 day_start, dtime()), timezone.get_current_timezone())
119 119 day_time_end = timezone.make_aware(datetime.combine(
120 120 day_end, dtime()), timezone.get_current_timezone())
121 121
122 122 posts_per_period = float(self.filter(
123 123 pub_time__lte=day_time_end,
124 124 pub_time__gte=day_time_start).count())
125 125
126 126 ppd = posts_per_period / POSTS_PER_DAY_RANGE
127 127
128 128 return ppd
@@ -1,173 +1,175 b''
1 1 import xml.etree.ElementTree as et
2 2 from django.db import transaction
3 3 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
4 4
5 5 ENCODING_UNICODE = 'unicode'
6 6
7 7 TAG_MODEL = 'model'
8 8 TAG_REQUEST = 'request'
9 9 TAG_RESPONSE = 'response'
10 10 TAG_ID = 'id'
11 11 TAG_STATUS = 'status'
12 12 TAG_MODELS = 'models'
13 13 TAG_TITLE = 'title'
14 14 TAG_TEXT = 'text'
15 15 TAG_THREAD = 'thread'
16 16 TAG_PUB_TIME = 'pub-time'
17 17 TAG_SIGNATURES = 'signatures'
18 18 TAG_SIGNATURE = 'signature'
19 19 TAG_CONTENT = 'content'
20 20 TAG_ATTACHMENTS = 'attachments'
21 21 TAG_ATTACHMENT = 'attachment'
22 22 TAG_TAGS = 'tags'
23 23 TAG_TAG = 'tag'
24 24
25 25 TYPE_GET = 'get'
26 26
27 27 ATTR_VERSION = 'version'
28 28 ATTR_TYPE = 'type'
29 29 ATTR_NAME = 'name'
30 30 ATTR_VALUE = 'value'
31 31 ATTR_MIMETYPE = 'mimetype'
32 32 ATTR_KEY = 'key'
33 33
34 34 STATUS_SUCCESS = 'success'
35 35
36 36
37 37 class SyncManager:
38 38 @staticmethod
39 39 def generate_response_get(model_list: list):
40 40 response = et.Element(TAG_RESPONSE)
41 41
42 42 status = et.SubElement(response, TAG_STATUS)
43 43 status.text = STATUS_SUCCESS
44 44
45 45 models = et.SubElement(response, TAG_MODELS)
46 46
47 47 for post in model_list:
48 48 model = et.SubElement(models, TAG_MODEL)
49 49 model.set(ATTR_NAME, 'post')
50 50
51 51 content_tag = et.SubElement(model, TAG_CONTENT)
52 52
53 53 tag_id = et.SubElement(content_tag, TAG_ID)
54 54 post.global_id.to_xml_element(tag_id)
55 55
56 56 title = et.SubElement(content_tag, TAG_TITLE)
57 57 title.text = post.title
58 58
59 59 text = et.SubElement(content_tag, TAG_TEXT)
60 60 text.text = post.get_sync_text()
61 61
62 62 thread = post.get_thread()
63 63 if post.is_opening():
64 64 tag_tags = et.SubElement(content_tag, TAG_TAGS)
65 65 for tag in thread.get_tags():
66 66 tag_tag = et.SubElement(tag_tags, TAG_TAG)
67 67 tag_tag.text = tag.name
68 68 else:
69 69 tag_thread = et.SubElement(content_tag, TAG_THREAD)
70 70 thread_id = et.SubElement(tag_thread, TAG_ID)
71 71 thread.get_opening_post().global_id.to_xml_element(thread_id)
72 72
73 73 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
74 74 pub_time.text = str(post.get_pub_time_str())
75 75
76 76 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
77 77 post_signatures = post.global_id.signature_set.all()
78 78 if post_signatures:
79 79 signatures = post_signatures
80 80 # TODO Adding signature to a post is not yet added. For now this
81 81 # block is useless
82 82 else:
83 83 # TODO Maybe the signature can be computed only once after
84 84 # the post is added? Need to add some on_save signal queue
85 85 # and add this there.
86 86 key = KeyPair.objects.get(public_key=post.global_id.key)
87 87 signatures = [Signature(
88 88 key_type=key.key_type,
89 89 key=key.public_key,
90 90 signature=key.sign(et.tostring(content_tag, ENCODING_UNICODE)),
91 91 )]
92 92 for signature in signatures:
93 93 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
94 94 signature_tag.set(ATTR_TYPE, signature.key_type)
95 95 signature_tag.set(ATTR_VALUE, signature.signature)
96 96 signature_tag.set(ATTR_KEY, signature.key)
97 97
98 98 return et.tostring(response, ENCODING_UNICODE)
99 99
100 100 @staticmethod
101 101 @transaction.atomic
102 102 def parse_response_get(response_xml):
103 103 tag_root = et.fromstring(response_xml)
104 104 tag_status = tag_root.find(TAG_STATUS)
105 105 if STATUS_SUCCESS == tag_status.text:
106 106 tag_models = tag_root.find(TAG_MODELS)
107 107 for tag_model in tag_models:
108 108 tag_content = tag_model.find(TAG_CONTENT)
109 109
110 valid = SyncManager._verify_model(tag_content, tag_model)
111
112 if not valid:
113 raise Exception('Invalid model signature')
110 signatures = SyncManager._verify_model(tag_content, tag_model)
114 111
115 112 tag_id = tag_content.find(TAG_ID)
116 113 global_id, exists = GlobalId.from_xml_element(tag_id)
117 114
118 115 if exists:
119 116 print('Post with same ID already exists')
120 117 else:
121 118 global_id.save()
119 for signature in signatures:
120 signature.global_id = global_id
121 signature.save()
122 122
123 123 title = tag_content.find(TAG_TITLE).text
124 124 text = tag_content.find(TAG_TEXT).text
125 125 pub_time = tag_content.find(TAG_PUB_TIME).text
126 126
127 127 thread = tag_content.find(TAG_THREAD)
128 128 tags = []
129 129 if thread:
130 130 opening_post = Post.objects.get(
131 131 id=thread.find(TAG_ID).text)
132 132 else:
133 133 opening_post = None
134 134 tag_tags = tag_content.find(TAG_TAGS)
135 135 for tag_tag in tag_tags:
136 136 tag, created = Tag.objects.get_or_create(name=tag_tag.text)
137 137 tags.append(tag)
138 138
139 139 # TODO Check that the replied posts are already present
140 140 # before adding new ones
141 141
142 142 # TODO Get images
143 143
144 144 post = Post.objects.import_post(
145 145 title=title, text=text, pub_time=pub_time,
146 opening_post=opening_post, tags=tags)
147 post.global_id = global_id
146 opening_post=opening_post, tags=tags,
147 global_id=global_id)
148 148 else:
149 149 # TODO Throw an exception?
150 150 pass
151 151
152 152 @staticmethod
153 153 def _verify_model(tag_content, tag_model):
154 154 """
155 155 Verifies all signatures for a single model.
156 156 """
157 157
158 valid = True
158 signatures = []
159 159
160 160 tag_signatures = tag_model.find(TAG_SIGNATURES)
161 161 for tag_signature in tag_signatures:
162 162 signature_type = tag_signature.get(ATTR_TYPE)
163 163 signature_value = tag_signature.get(ATTR_VALUE)
164 164 signature_key = tag_signature.get(ATTR_KEY)
165 165
166 signature = Signature(key_type=signature_type,
167 key=signature_key,
168 signature=signature_value)
169 signatures.append(signature)
170
166 171 if not KeyPair.objects.verify(
167 signature_key,
168 et.tostring(tag_content, ENCODING_UNICODE),
169 signature_value, signature_type):
170 valid = False
171 break
172 signature, et.tostring(tag_content, ENCODING_UNICODE)):
173 raise Exception('Invalid model signature')
172 174
173 return valid
175 return signatures
@@ -1,61 +1,61 b''
1 1 import base64
2 2 from ecdsa import SigningKey, VerifyingKey, BadSignatureError
3 3 from django.db import models
4 4
5 5 TYPE_ECDSA = 'ecdsa'
6 6
7 7 APP_LABEL_BOARDS = 'boards'
8 8
9 9
10 10 class KeyPairManager(models.Manager):
11 11 def generate_key(self, key_type=TYPE_ECDSA, primary=False):
12 12 if primary and self.filter(primary=True).exists():
13 13 raise Exception('There can be only one primary key')
14 14
15 15 if key_type == TYPE_ECDSA:
16 16 private = SigningKey.generate()
17 17 public = private.get_verifying_key()
18 18
19 19 private_key_str = base64.b64encode(private.to_string()).decode()
20 20 public_key_str = base64.b64encode(public.to_string()).decode()
21 21
22 22 return self.create(public_key=public_key_str,
23 23 private_key=private_key_str,
24 24 key_type=TYPE_ECDSA, primary=primary)
25 25 else:
26 26 raise Exception('Key type not supported')
27 27
28 def verify(self, public_key_str, string, signature, key_type=TYPE_ECDSA):
29 if key_type == TYPE_ECDSA:
30 public = VerifyingKey.from_string(base64.b64decode(public_key_str))
31 signature_byte = base64.b64decode(signature)
28 def verify(self, signature, string):
29 if signature.key_type == TYPE_ECDSA:
30 public = VerifyingKey.from_string(base64.b64decode(signature.key))
31 signature_byte = base64.b64decode(signature.signature)
32 32 try:
33 33 return public.verify(signature_byte, string.encode())
34 34 except BadSignatureError:
35 35 return False
36 36 else:
37 37 raise Exception('Key type not supported')
38 38
39 39 def has_primary(self):
40 40 return self.filter(primary=True).exists()
41 41
42 42
43 43 class KeyPair(models.Model):
44 44 class Meta:
45 45 app_label = APP_LABEL_BOARDS
46 46
47 47 objects = KeyPairManager()
48 48
49 49 public_key = models.TextField()
50 50 private_key = models.TextField()
51 51 key_type = models.TextField()
52 52 primary = models.BooleanField(default=False)
53 53
54 54 def __str__(self):
55 55 return '%s::%s' % (self.key_type, self.public_key)
56 56
57 57 def sign(self, string):
58 58 private = SigningKey.from_string(base64.b64decode(
59 59 self.private_key.encode()))
60 60 signature_byte = private.sign_deterministic(string.encode())
61 61 return base64.b64encode(signature_byte).decode()
@@ -1,87 +1,89 b''
1 1 from base64 import b64encode
2 2 import logging
3 3
4 4 from django.test import TestCase
5 from boards.models import KeyPair, GlobalId, Post
5 from boards.models import KeyPair, GlobalId, Post, Signature
6 6 from boards.models.post.sync import SyncManager
7 7
8 8 logger = logging.getLogger(__name__)
9 9
10 10
11 11 class KeyTest(TestCase):
12 12 def test_create_key(self):
13 13 key = KeyPair.objects.generate_key('ecdsa')
14 14
15 15 self.assertIsNotNone(key, 'The key was not created.')
16 16
17 17 def test_validation(self):
18 18 key = KeyPair.objects.generate_key(key_type='ecdsa')
19 19 message = 'msg'
20 signature = key.sign(message)
21 valid = KeyPair.objects.verify(key.public_key, message, signature,
22 key_type='ecdsa')
20 signature_value = key.sign(message)
21
22 signature = Signature(key_type='ecdsa', key=key.public_key,
23 signature=signature_value)
24 valid = KeyPair.objects.verify(signature, message)
23 25
24 26 self.assertTrue(valid, 'Message verification failed.')
25 27
26 28 def test_primary_constraint(self):
27 29 KeyPair.objects.generate_key(key_type='ecdsa', primary=True)
28 30
29 31 with self.assertRaises(Exception):
30 32 KeyPair.objects.generate_key(key_type='ecdsa', primary=True)
31 33
32 34 def test_model_id_save(self):
33 35 model_id = GlobalId(key_type='test', key='test key', local_id='1')
34 36 model_id.save()
35 37
36 38 def test_request_get(self):
37 39 post = self._create_post_with_key()
38 40
39 41 request = GlobalId.objects.generate_request_get([post.global_id])
40 42 logger.debug(request)
41 43
42 44 key = KeyPair.objects.get(primary=True)
43 45 self.assertTrue('<request type="get" version="1.0">'
44 46 '<model name="post" version="1.0">'
45 47 '<id key="%s" local-id="1" type="%s" />'
46 48 '</model>'
47 49 '</request>' % (
48 50 key.public_key,
49 51 key.key_type,
50 52 ) in request,
51 53 'Wrong XML generated for the GET request.')
52 54
53 55 def test_response_get(self):
54 56 post = self._create_post_with_key()
55 57 reply_post = Post.objects.create_post(title='test_title',
56 58 text='[post]%d[/post]' % post.id,
57 59 thread=post.get_thread())
58 60
59 61 response = SyncManager.generate_response_get([reply_post])
60 62 logger.debug(response)
61 63
62 64 key = KeyPair.objects.get(primary=True)
63 65 self.assertTrue('<status>success</status>'
64 66 '<models>'
65 67 '<model name="post">'
66 68 '<content>'
67 69 '<id key="%s" local-id="%d" type="%s" />'
68 70 '<title>test_title</title>'
69 71 '<text>[post]%s[/post]</text>'
70 72 '<thread><id key="%s" local-id="%d" type="%s" /></thread>'
71 73 '<pub-time>%s</pub-time>'
72 74 '</content>' % (
73 75 key.public_key,
74 76 reply_post.id,
75 77 key.key_type,
76 78 str(post.global_id),
77 79 key.public_key,
78 80 post.id,
79 81 key.key_type,
80 82 str(reply_post.get_pub_time_str()),
81 83 ) in response,
82 84 'Wrong XML generated for the GET response.')
83 85
84 86 def _create_post_with_key(self):
85 87 KeyPair.objects.generate_key(primary=True)
86 88
87 89 return Post.objects.create_post(title='test_title', text='test_text')
@@ -1,69 +1,72 b''
1 1 from boards.models import KeyPair, Post, Tag
2 2 from boards.models.post.sync import SyncManager
3 3 from boards.tests.mocks import MockRequest
4 4 from boards.views.sync import response_get
5 5
6 6 __author__ = 'neko259'
7 7
8 8
9 9 from django.test import TestCase
10 10
11 11
12 12 class SyncTest(TestCase):
13 13 def test_get(self):
14 14 """
15 15 Forms a GET request of a post and checks the response.
16 16 """
17 17
18 18 KeyPair.objects.generate_key(primary=True)
19 19 tag = Tag.objects.create(name='tag1')
20 20 post = Post.objects.create_post(title='test_title', text='test_text',
21 21 tags=[tag])
22 22
23 23 request = MockRequest()
24 24 request.body = (
25 25 '<request type="get" version="1.0">'
26 26 '<model name="post" version="1.0">'
27 27 '<id key="%s" local-id="%d" type="%s" />'
28 28 '</model>'
29 29 '</request>' % (post.global_id.key,
30 30 post.id,
31 31 post.global_id.key_type)
32 32 )
33 33
34 34 response = response_get(request).content.decode()
35 35 self.assertTrue(
36 36 '<status>success</status>'
37 37 '<models>'
38 38 '<model name="post">'
39 39 '<content>'
40 40 '<id key="%s" local-id="%d" type="%s" />'
41 41 '<title>%s</title>'
42 42 '<text>%s</text>'
43 43 '<tags><tag>%s</tag></tags>'
44 44 '<pub-time>%s</pub-time>'
45 45 '</content>' % (
46 46 post.global_id.key,
47 47 post.id,
48 48 post.global_id.key_type,
49 49 post.title,
50 50 post.get_raw_text(),
51 51 post.get_thread().get_tags().first().name,
52 52 post.get_pub_time_str(),
53 53 ) in response_get(request).content.decode(),
54 54 'Wrong response generated for the GET request.')
55 55
56 56 post.delete()
57 57
58 58 SyncManager.parse_response_get(response)
59 59 self.assertEqual(1, Post.objects.count(),
60 60 'Post was not created from XML response.')
61 61
62 62 parsed_post = Post.objects.first()
63 63 self.assertEqual('tag1',
64 64 parsed_post.get_thread().get_tags().first().name,
65 65 'Invalid tag was parsed.')
66 66
67 67 SyncManager.parse_response_get(response)
68 68 self.assertEqual(1, Post.objects.count(),
69 69 'The same post was imported twice.')
70
71 self.assertEqual(1, parsed_post.global_id.signature_set.count(),
72 'Signature was not saved.')
General Comments 0
You need to be logged in to leave comments. Login now