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