##// END OF EJS Templates
Check that parsed post has a signature of its author's key
neko259 -
r1582:9e8b0925 default
parent child Browse files
Show More
@@ -1,291 +1,299 b''
1 import xml.etree.ElementTree as et
1 import xml.etree.ElementTree as et
2
2
3 from boards.models.attachment.downloaders import download
3 from boards.models.attachment.downloaders import download
4 from boards.utils import get_file_mimetype, get_file_hash
4 from boards.utils import get_file_mimetype, get_file_hash
5 from django.db import transaction
5 from django.db import transaction
6 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
6 from boards.models import KeyPair, GlobalId, Signature, Post, Tag
7
7
8 EXCEPTION_NODE = 'Sync node returned an error: {}'
8 EXCEPTION_NODE = 'Sync node returned an error: {}.'
9 EXCEPTION_OP = 'Load the OP first'
9 EXCEPTION_OP = 'Load the OP first.'
10 EXCEPTION_DOWNLOAD = 'File was not downloaded'
10 EXCEPTION_DOWNLOAD = 'File was not downloaded.'
11 EXCEPTION_HASH = 'File hash does not match attachment hash'
11 EXCEPTION_HASH = 'File hash does not match attachment hash.'
12 EXCEPTION_SIGNATURE = 'Invalid model signature for {}'
12 EXCEPTION_SIGNATURE = 'Invalid model signature for {}.'
13 EXCEPTION_AUTHOR_SIGNATURE = 'Model {} has no author signature.'
13 ENCODING_UNICODE = 'unicode'
14 ENCODING_UNICODE = 'unicode'
14
15
15 TAG_MODEL = 'model'
16 TAG_MODEL = 'model'
16 TAG_REQUEST = 'request'
17 TAG_REQUEST = 'request'
17 TAG_RESPONSE = 'response'
18 TAG_RESPONSE = 'response'
18 TAG_ID = 'id'
19 TAG_ID = 'id'
19 TAG_STATUS = 'status'
20 TAG_STATUS = 'status'
20 TAG_MODELS = 'models'
21 TAG_MODELS = 'models'
21 TAG_TITLE = 'title'
22 TAG_TITLE = 'title'
22 TAG_TEXT = 'text'
23 TAG_TEXT = 'text'
23 TAG_THREAD = 'thread'
24 TAG_THREAD = 'thread'
24 TAG_PUB_TIME = 'pub-time'
25 TAG_PUB_TIME = 'pub-time'
25 TAG_SIGNATURES = 'signatures'
26 TAG_SIGNATURES = 'signatures'
26 TAG_SIGNATURE = 'signature'
27 TAG_SIGNATURE = 'signature'
27 TAG_CONTENT = 'content'
28 TAG_CONTENT = 'content'
28 TAG_ATTACHMENTS = 'attachments'
29 TAG_ATTACHMENTS = 'attachments'
29 TAG_ATTACHMENT = 'attachment'
30 TAG_ATTACHMENT = 'attachment'
30 TAG_TAGS = 'tags'
31 TAG_TAGS = 'tags'
31 TAG_TAG = 'tag'
32 TAG_TAG = 'tag'
32 TAG_ATTACHMENT_REFS = 'attachment-refs'
33 TAG_ATTACHMENT_REFS = 'attachment-refs'
33 TAG_ATTACHMENT_REF = 'attachment-ref'
34 TAG_ATTACHMENT_REF = 'attachment-ref'
34 TAG_TRIPCODE = 'tripcode'
35 TAG_TRIPCODE = 'tripcode'
35 TAG_VERSION = 'version'
36 TAG_VERSION = 'version'
36
37
37 TYPE_GET = 'get'
38 TYPE_GET = 'get'
38
39
39 ATTR_VERSION = 'version'
40 ATTR_VERSION = 'version'
40 ATTR_TYPE = 'type'
41 ATTR_TYPE = 'type'
41 ATTR_NAME = 'name'
42 ATTR_NAME = 'name'
42 ATTR_VALUE = 'value'
43 ATTR_VALUE = 'value'
43 ATTR_MIMETYPE = 'mimetype'
44 ATTR_MIMETYPE = 'mimetype'
44 ATTR_KEY = 'key'
45 ATTR_KEY = 'key'
45 ATTR_REF = 'ref'
46 ATTR_REF = 'ref'
46 ATTR_URL = 'url'
47 ATTR_URL = 'url'
47 ATTR_ID_TYPE = 'id-type'
48 ATTR_ID_TYPE = 'id-type'
48
49
49 ID_TYPE_MD5 = 'md5'
50 ID_TYPE_MD5 = 'md5'
50
51
51 STATUS_SUCCESS = 'success'
52 STATUS_SUCCESS = 'success'
52
53
53
54
54 class SyncException(Exception):
55 class SyncException(Exception):
55 pass
56 pass
56
57
57
58
58 class SyncManager:
59 class SyncManager:
59 @staticmethod
60 @staticmethod
60 def generate_response_get(model_list: list):
61 def generate_response_get(model_list: list):
61 response = et.Element(TAG_RESPONSE)
62 response = et.Element(TAG_RESPONSE)
62
63
63 status = et.SubElement(response, TAG_STATUS)
64 status = et.SubElement(response, TAG_STATUS)
64 status.text = STATUS_SUCCESS
65 status.text = STATUS_SUCCESS
65
66
66 models = et.SubElement(response, TAG_MODELS)
67 models = et.SubElement(response, TAG_MODELS)
67
68
68 for post in model_list:
69 for post in model_list:
69 model = et.SubElement(models, TAG_MODEL)
70 model = et.SubElement(models, TAG_MODEL)
70 model.set(ATTR_NAME, 'post')
71 model.set(ATTR_NAME, 'post')
71
72
72 global_id = post.global_id
73 global_id = post.global_id
73
74
74 images = post.images.all()
75 images = post.images.all()
75 attachments = post.attachments.all()
76 attachments = post.attachments.all()
76 if global_id.content:
77 if global_id.content:
77 model.append(et.fromstring(global_id.content))
78 model.append(et.fromstring(global_id.content))
78 if len(images) > 0 or len(attachments) > 0:
79 if len(images) > 0 or len(attachments) > 0:
79 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
80 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
80 for image in images:
81 for image in images:
81 SyncManager._attachment_to_xml(
82 SyncManager._attachment_to_xml(
82 None, attachment_refs, image.image.file,
83 None, attachment_refs, image.image.file,
83 image.hash, image.image.url)
84 image.hash, image.image.url)
84 for file in attachments:
85 for file in attachments:
85 SyncManager._attachment_to_xml(
86 SyncManager._attachment_to_xml(
86 None, attachment_refs, file.file.file,
87 None, attachment_refs, file.file.file,
87 file.hash, file.file.url)
88 file.hash, file.file.url)
88 else:
89 else:
89 content_tag = et.SubElement(model, TAG_CONTENT)
90 content_tag = et.SubElement(model, TAG_CONTENT)
90
91
91 tag_id = et.SubElement(content_tag, TAG_ID)
92 tag_id = et.SubElement(content_tag, TAG_ID)
92 global_id.to_xml_element(tag_id)
93 global_id.to_xml_element(tag_id)
93
94
94 title = et.SubElement(content_tag, TAG_TITLE)
95 title = et.SubElement(content_tag, TAG_TITLE)
95 title.text = post.title
96 title.text = post.title
96
97
97 text = et.SubElement(content_tag, TAG_TEXT)
98 text = et.SubElement(content_tag, TAG_TEXT)
98 text.text = post.get_sync_text()
99 text.text = post.get_sync_text()
99
100
100 thread = post.get_thread()
101 thread = post.get_thread()
101 if post.is_opening():
102 if post.is_opening():
102 tag_tags = et.SubElement(content_tag, TAG_TAGS)
103 tag_tags = et.SubElement(content_tag, TAG_TAGS)
103 for tag in thread.get_tags():
104 for tag in thread.get_tags():
104 tag_tag = et.SubElement(tag_tags, TAG_TAG)
105 tag_tag = et.SubElement(tag_tags, TAG_TAG)
105 tag_tag.text = tag.name
106 tag_tag.text = tag.name
106 else:
107 else:
107 tag_thread = et.SubElement(content_tag, TAG_THREAD)
108 tag_thread = et.SubElement(content_tag, TAG_THREAD)
108 thread_id = et.SubElement(tag_thread, TAG_ID)
109 thread_id = et.SubElement(tag_thread, TAG_ID)
109 thread.get_opening_post().global_id.to_xml_element(thread_id)
110 thread.get_opening_post().global_id.to_xml_element(thread_id)
110
111
111 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
112 pub_time = et.SubElement(content_tag, TAG_PUB_TIME)
112 pub_time.text = str(post.get_pub_time_str())
113 pub_time.text = str(post.get_pub_time_str())
113
114
114 if post.tripcode:
115 if post.tripcode:
115 tripcode = et.SubElement(content_tag, TAG_TRIPCODE)
116 tripcode = et.SubElement(content_tag, TAG_TRIPCODE)
116 tripcode.text = post.tripcode
117 tripcode.text = post.tripcode
117
118
118 if len(images) > 0 or len(attachments) > 0:
119 if len(images) > 0 or len(attachments) > 0:
119 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
120 attachments_tag = et.SubElement(content_tag, TAG_ATTACHMENTS)
120 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
121 attachment_refs = et.SubElement(model, TAG_ATTACHMENT_REFS)
121
122
122 for image in images:
123 for image in images:
123 SyncManager._attachment_to_xml(
124 SyncManager._attachment_to_xml(
124 attachments_tag, attachment_refs, image.image.file,
125 attachments_tag, attachment_refs, image.image.file,
125 image.hash, image.image.url)
126 image.hash, image.image.url)
126 for file in attachments:
127 for file in attachments:
127 SyncManager._attachment_to_xml(
128 SyncManager._attachment_to_xml(
128 attachments_tag, attachment_refs, file.file.file,
129 attachments_tag, attachment_refs, file.file.file,
129 file.hash, file.file.url)
130 file.hash, file.file.url)
130 version_tag = et.SubElement(content_tag, TAG_VERSION)
131 version_tag = et.SubElement(content_tag, TAG_VERSION)
131 version_tag.text = str(post.version)
132 version_tag.text = str(post.version)
132
133
133 global_id.content = et.tostring(content_tag, ENCODING_UNICODE)
134 global_id.content = et.tostring(content_tag, ENCODING_UNICODE)
134 global_id.save()
135 global_id.save()
135
136
136 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
137 signatures_tag = et.SubElement(model, TAG_SIGNATURES)
137 post_signatures = global_id.signature_set.all()
138 post_signatures = global_id.signature_set.all()
138 if post_signatures:
139 if post_signatures:
139 signatures = post_signatures
140 signatures = post_signatures
140 else:
141 else:
141 key = KeyPair.objects.get(public_key=global_id.key)
142 key = KeyPair.objects.get(public_key=global_id.key)
142 signature = Signature(
143 signature = Signature(
143 key_type=key.key_type,
144 key_type=key.key_type,
144 key=key.public_key,
145 key=key.public_key,
145 signature=key.sign(global_id.content),
146 signature=key.sign(global_id.content),
146 global_id=global_id,
147 global_id=global_id,
147 )
148 )
148 signature.save()
149 signature.save()
149 signatures = [signature]
150 signatures = [signature]
150 for signature in signatures:
151 for signature in signatures:
151 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
152 signature_tag = et.SubElement(signatures_tag, TAG_SIGNATURE)
152 signature_tag.set(ATTR_TYPE, signature.key_type)
153 signature_tag.set(ATTR_TYPE, signature.key_type)
153 signature_tag.set(ATTR_VALUE, signature.signature)
154 signature_tag.set(ATTR_VALUE, signature.signature)
154 signature_tag.set(ATTR_KEY, signature.key)
155 signature_tag.set(ATTR_KEY, signature.key)
155
156
156 return et.tostring(response, ENCODING_UNICODE)
157 return et.tostring(response, ENCODING_UNICODE)
157
158
158 @staticmethod
159 @staticmethod
159 @transaction.atomic
160 @transaction.atomic
160 def parse_response_get(response_xml, hostname):
161 def parse_response_get(response_xml, hostname):
161 tag_root = et.fromstring(response_xml)
162 tag_root = et.fromstring(response_xml)
162 tag_status = tag_root.find(TAG_STATUS)
163 tag_status = tag_root.find(TAG_STATUS)
163 if STATUS_SUCCESS == tag_status.text:
164 if STATUS_SUCCESS == tag_status.text:
164 tag_models = tag_root.find(TAG_MODELS)
165 tag_models = tag_root.find(TAG_MODELS)
165 for tag_model in tag_models:
166 for tag_model in tag_models:
166 tag_content = tag_model.find(TAG_CONTENT)
167 tag_content = tag_model.find(TAG_CONTENT)
167
168
168 content_str = et.tostring(tag_content, ENCODING_UNICODE)
169 content_str = et.tostring(tag_content, ENCODING_UNICODE)
169 signatures = SyncManager._verify_model(content_str, tag_model)
170
170
171 tag_id = tag_content.find(TAG_ID)
171 tag_id = tag_content.find(TAG_ID)
172 global_id, exists = GlobalId.from_xml_element(tag_id)
172 global_id, exists = GlobalId.from_xml_element(tag_id)
173 signatures = SyncManager._verify_model(global_id, content_str, tag_model)
173
174
174 if exists:
175 if exists:
175 print('Post with same ID already exists')
176 print('Post with same ID already exists')
176 else:
177 else:
177 global_id.content = content_str
178 global_id.content = content_str
178 global_id.save()
179 global_id.save()
179 for signature in signatures:
180 for signature in signatures:
180 signature.global_id = global_id
181 signature.global_id = global_id
181 signature.save()
182 signature.save()
182
183
183 title = tag_content.find(TAG_TITLE).text or ''
184 title = tag_content.find(TAG_TITLE).text or ''
184 text = tag_content.find(TAG_TEXT).text or ''
185 text = tag_content.find(TAG_TEXT).text or ''
185 pub_time = tag_content.find(TAG_PUB_TIME).text
186 pub_time = tag_content.find(TAG_PUB_TIME).text
186 tripcode_tag = tag_content.find(TAG_TRIPCODE)
187 tripcode_tag = tag_content.find(TAG_TRIPCODE)
187 if tripcode_tag is not None:
188 if tripcode_tag is not None:
188 tripcode = tripcode_tag.text or ''
189 tripcode = tripcode_tag.text or ''
189 else:
190 else:
190 tripcode = ''
191 tripcode = ''
191
192
192 thread = tag_content.find(TAG_THREAD)
193 thread = tag_content.find(TAG_THREAD)
193 tags = []
194 tags = []
194 if thread:
195 if thread:
195 thread_id = thread.find(TAG_ID)
196 thread_id = thread.find(TAG_ID)
196 op_global_id, exists = GlobalId.from_xml_element(thread_id)
197 op_global_id, exists = GlobalId.from_xml_element(thread_id)
197 if exists:
198 if exists:
198 opening_post = Post.objects.get(global_id=op_global_id)
199 opening_post = Post.objects.get(global_id=op_global_id)
199 else:
200 else:
200 raise SyncException(EXCEPTION_OP)
201 raise SyncException(EXCEPTION_OP)
201 else:
202 else:
202 opening_post = None
203 opening_post = None
203 tag_tags = tag_content.find(TAG_TAGS)
204 tag_tags = tag_content.find(TAG_TAGS)
204 for tag_tag in tag_tags:
205 for tag_tag in tag_tags:
205 tag, created = Tag.objects.get_or_create(
206 tag, created = Tag.objects.get_or_create(
206 name=tag_tag.text)
207 name=tag_tag.text)
207 tags.append(tag)
208 tags.append(tag)
208
209
209 # TODO Check that the replied posts are already present
210 # TODO Check that the replied posts are already present
210 # before adding new ones
211 # before adding new ones
211
212
212 files = []
213 files = []
213 tag_attachments = tag_content.find(TAG_ATTACHMENTS) or list()
214 tag_attachments = tag_content.find(TAG_ATTACHMENTS) or list()
214 tag_refs = tag_model.find(TAG_ATTACHMENT_REFS)
215 tag_refs = tag_model.find(TAG_ATTACHMENT_REFS)
215 for attachment in tag_attachments:
216 for attachment in tag_attachments:
216 tag_ref = tag_refs.find("{}[@ref='{}']".format(
217 tag_ref = tag_refs.find("{}[@ref='{}']".format(
217 TAG_ATTACHMENT_REF, attachment.text))
218 TAG_ATTACHMENT_REF, attachment.text))
218 url = tag_ref.get(ATTR_URL)
219 url = tag_ref.get(ATTR_URL)
219 attached_file = download(hostname + url)
220 attached_file = download(hostname + url)
220 if attached_file is None:
221 if attached_file is None:
221 raise SyncException(EXCEPTION_DOWNLOAD)
222 raise SyncException(EXCEPTION_DOWNLOAD)
222
223
223 hash = get_file_hash(attached_file)
224 hash = get_file_hash(attached_file)
224 if hash != attachment.text:
225 if hash != attachment.text:
225 raise SyncException(EXCEPTION_HASH)
226 raise SyncException(EXCEPTION_HASH)
226
227
227 files.append(attached_file)
228 files.append(attached_file)
228
229
229 Post.objects.import_post(
230 Post.objects.import_post(
230 title=title, text=text, pub_time=pub_time,
231 title=title, text=text, pub_time=pub_time,
231 opening_post=opening_post, tags=tags,
232 opening_post=opening_post, tags=tags,
232 global_id=global_id, files=files, tripcode=tripcode)
233 global_id=global_id, files=files, tripcode=tripcode)
233 print('Parsed post {}'.format(global_id))
234 print('Parsed post {}'.format(global_id))
234 else:
235 else:
235 raise SyncException(EXCEPTION_NODE.format(tag_status.text))
236 raise SyncException(EXCEPTION_NODE.format(tag_status.text))
236
237
237 @staticmethod
238 @staticmethod
238 def generate_response_list():
239 def generate_response_list():
239 response = et.Element(TAG_RESPONSE)
240 response = et.Element(TAG_RESPONSE)
240
241
241 status = et.SubElement(response, TAG_STATUS)
242 status = et.SubElement(response, TAG_STATUS)
242 status.text = STATUS_SUCCESS
243 status.text = STATUS_SUCCESS
243
244
244 models = et.SubElement(response, TAG_MODELS)
245 models = et.SubElement(response, TAG_MODELS)
245
246
246 for post in Post.objects.prefetch_related('global_id').all():
247 for post in Post.objects.prefetch_related('global_id').all():
247 tag_model = et.SubElement(models, TAG_MODEL)
248 tag_model = et.SubElement(models, TAG_MODEL)
248 tag_id = et.SubElement(tag_model, TAG_ID)
249 tag_id = et.SubElement(tag_model, TAG_ID)
249 post.global_id.to_xml_element(tag_id)
250 post.global_id.to_xml_element(tag_id)
250 tag_version = et.SubElement(tag_model, TAG_VERSION)
251 tag_version = et.SubElement(tag_model, TAG_VERSION)
251 tag_version.text = str(post.version)
252 tag_version.text = str(post.version)
252
253
253 return et.tostring(response, ENCODING_UNICODE)
254 return et.tostring(response, ENCODING_UNICODE)
254
255
255 @staticmethod
256 @staticmethod
256 def _verify_model(content_str, tag_model):
257 def _verify_model(global_id, content_str, tag_model):
257 """
258 """
258 Verifies all signatures for a single model.
259 Verifies all signatures for a single model.
259 """
260 """
260
261
261 signatures = []
262 signatures = []
262
263
263 tag_signatures = tag_model.find(TAG_SIGNATURES)
264 tag_signatures = tag_model.find(TAG_SIGNATURES)
265 has_author_signature = False
264 for tag_signature in tag_signatures:
266 for tag_signature in tag_signatures:
265 signature_type = tag_signature.get(ATTR_TYPE)
267 signature_type = tag_signature.get(ATTR_TYPE)
266 signature_value = tag_signature.get(ATTR_VALUE)
268 signature_value = tag_signature.get(ATTR_VALUE)
267 signature_key = tag_signature.get(ATTR_KEY)
269 signature_key = tag_signature.get(ATTR_KEY)
268
270
271 if global_id.key_type == signature_type and\
272 global_id.key == signature_key:
273 has_author_signature = True
274
269 signature = Signature(key_type=signature_type,
275 signature = Signature(key_type=signature_type,
270 key=signature_key,
276 key=signature_key,
271 signature=signature_value)
277 signature=signature_value)
272
278
273 if not KeyPair.objects.verify(signature, content_str):
279 if not KeyPair.objects.verify(signature, content_str):
274 raise SyncException(EXCEPTION_SIGNATURE.format(content_str))
280 raise SyncException(EXCEPTION_SIGNATURE.format(content_str))
275
281
276 signatures.append(signature)
282 signatures.append(signature)
283 if not has_author_signature:
284 raise SyncException(EXCEPTION_AUTHOR_SIGNATURE.format(content_str))
277
285
278 return signatures
286 return signatures
279
287
280 @staticmethod
288 @staticmethod
281 def _attachment_to_xml(tag_attachments, tag_refs, file, hash, url):
289 def _attachment_to_xml(tag_attachments, tag_refs, file, hash, url):
282 if tag_attachments is not None:
290 if tag_attachments is not None:
283 mimetype = get_file_mimetype(file)
291 mimetype = get_file_mimetype(file)
284 attachment = et.SubElement(tag_attachments, TAG_ATTACHMENT)
292 attachment = et.SubElement(tag_attachments, TAG_ATTACHMENT)
285 attachment.set(ATTR_MIMETYPE, mimetype)
293 attachment.set(ATTR_MIMETYPE, mimetype)
286 attachment.set(ATTR_ID_TYPE, ID_TYPE_MD5)
294 attachment.set(ATTR_ID_TYPE, ID_TYPE_MD5)
287 attachment.text = hash
295 attachment.text = hash
288
296
289 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
297 attachment_ref = et.SubElement(tag_refs, TAG_ATTACHMENT_REF)
290 attachment_ref.set(ATTR_REF, hash)
298 attachment_ref.set(ATTR_REF, hash)
291 attachment_ref.set(ATTR_URL, url)
299 attachment_ref.set(ATTR_URL, url)
General Comments 0
You need to be logged in to leave comments. Login now