Show More
@@ -0,0 +1,17 b'' | |||
|
1 | __author__ = 'neko259' | |
|
2 | ||
|
3 | ||
|
4 | from django.core.management import BaseCommand | |
|
5 | from django.db import transaction | |
|
6 | ||
|
7 | from boards.models import KeyPair | |
|
8 | ||
|
9 | ||
|
10 | class Command(BaseCommand): | |
|
11 | help = 'Generates the new keypair. The first one will be primary.' | |
|
12 | ||
|
13 | @transaction.atomic | |
|
14 | def handle(self, *args, **options): | |
|
15 | key = KeyPair.objects.generate_key( | |
|
16 | primary=not KeyPair.objects.has_primary()) | |
|
17 | print(key) No newline at end of file |
@@ -0,0 +1,55 b'' | |||
|
1 | from boards.models import KeyPair, Post | |
|
2 | from boards.tests.mocks import MockRequest | |
|
3 | from boards.views.sync import respond_get | |
|
4 | ||
|
5 | __author__ = 'neko259' | |
|
6 | ||
|
7 | ||
|
8 | from django.test import TestCase | |
|
9 | ||
|
10 | ||
|
11 | class SyncTest(TestCase): | |
|
12 | def test_get(self): | |
|
13 | """ | |
|
14 | Forms a GET request of a post and checks the response. | |
|
15 | """ | |
|
16 | ||
|
17 | key = KeyPair(public_key='pubkey', private_key='privkey', | |
|
18 | key_type='test_key_type', primary=True) | |
|
19 | key.save() | |
|
20 | ||
|
21 | post = Post.objects.create_post(title='test_title', text='test_text') | |
|
22 | ||
|
23 | request = MockRequest() | |
|
24 | request.POST['xml'] = ( | |
|
25 | '<request type="get" version="1.0">' | |
|
26 | '<model name="post" version="1.0">' | |
|
27 | '<id key="%s" local-id="%d" type="%s" />' | |
|
28 | '</model>' | |
|
29 | '</request>' % (post.global_id.key, | |
|
30 | post.id, | |
|
31 | post.global_id.key_type) | |
|
32 | ) | |
|
33 | ||
|
34 | self.assertTrue( | |
|
35 | '<response>' | |
|
36 | '<status>success</status>' | |
|
37 | '<models>' | |
|
38 | '<model name="post" ref-id="1">' | |
|
39 | '<id key="%s" local-id="%d" type="%s" />' | |
|
40 | '<title>%s</title>' | |
|
41 | '<text>%s</text>' | |
|
42 | '<pub-time>%d</pub-time>' | |
|
43 | '<edit-time>%d</edit-time>' | |
|
44 | '</model>' | |
|
45 | '</models>' | |
|
46 | '</response>' % ( | |
|
47 | post.global_id.key, | |
|
48 | post.id, | |
|
49 | post.global_id.key_type, | |
|
50 | post.title, | |
|
51 | post.text.raw, | |
|
52 | post.get_pub_time_epoch(), | |
|
53 | post.get_edit_time_epoch(), | |
|
54 | ) in respond_get(request).content.decode(), | |
|
55 | 'Wrong response generated for the GET request.') No newline at end of file |
@@ -1,45 +1,67 b'' | |||
|
1 | 1 | import xml.etree.ElementTree as et |
|
2 | 2 | from django.db import models |
|
3 | 3 | |
|
4 | 4 | |
|
5 | 5 | ATTR_KEY = 'key' |
|
6 | 6 | ATTR_KEY_TYPE = 'type' |
|
7 | 7 | ATTR_LOCAL_ID = 'local-id' |
|
8 | 8 | |
|
9 | 9 | |
|
10 | 10 | class GlobalId(models.Model): |
|
11 | 11 | class Meta: |
|
12 | 12 | app_label = 'boards' |
|
13 | 13 | |
|
14 | 14 | def __init__(self, *args, **kwargs): |
|
15 | 15 | models.Model.__init__(self, *args, **kwargs) |
|
16 | 16 | |
|
17 | 17 | if 'key' in kwargs and 'key_type' in kwargs and 'local_id' in kwargs: |
|
18 | 18 | self.key = kwargs['key'] |
|
19 | 19 | self.key_type = kwargs['key_type'] |
|
20 | 20 | self.local_id = kwargs['local_id'] |
|
21 | 21 | |
|
22 | 22 | key = models.TextField() |
|
23 | 23 | key_type = models.TextField() |
|
24 | 24 | local_id = models.IntegerField() |
|
25 | 25 | |
|
26 | 26 | def __str__(self): |
|
27 |
return '%s |
|
|
27 | return '%s | %s | %d' % (self.key_type, self.key, self.local_id) | |
|
28 | 28 | |
|
29 |
def to_xml_element(self, element: et. |
|
|
29 | def to_xml_element(self, element: et.Element): | |
|
30 | 30 | """ |
|
31 | 31 | Exports global id to an XML element. |
|
32 | 32 | """ |
|
33 | 33 | |
|
34 | 34 | element.set(ATTR_KEY, self.key) |
|
35 | 35 | element.set(ATTR_KEY_TYPE, self.key_type) |
|
36 | 36 | element.set(ATTR_LOCAL_ID, str(self.local_id)) |
|
37 | 37 | |
|
38 | @staticmethod | |
|
39 | def from_xml_element(element: et.Element, existing=False): | |
|
40 | """ | |
|
41 | Parses XML id tag and gets global id from it. | |
|
42 | ||
|
43 | Arguments: | |
|
44 | element -- the XML 'id' element | |
|
45 | existing -- if this is False, a new instance of GlobalId will be | |
|
46 | created. Otherwise, we will search for an existing GlobalId instance | |
|
47 | and throw DoesNotExist if there isn't one. | |
|
48 | """ | |
|
49 | ||
|
50 | if existing: | |
|
51 | return GlobalId.objects.get(key=element.get(ATTR_KEY), | |
|
52 | key_type=element.get(ATTR_KEY_TYPE), | |
|
53 | local_id=int(element.get( | |
|
54 | ATTR_LOCAL_ID))) | |
|
55 | else: | |
|
56 | return GlobalId(key=element.get(ATTR_KEY), | |
|
57 | key_type=element.get(ATTR_KEY_TYPE), | |
|
58 | local_id=int(element.get(ATTR_LOCAL_ID))) | |
|
59 | ||
|
38 | 60 | |
|
39 | 61 | class Signature(models.Model): |
|
40 | 62 | class Meta: |
|
41 | 63 | app_label = 'boards' |
|
42 | 64 | |
|
43 | 65 | key_type = models.TextField() |
|
44 | 66 | key = models.TextField() |
|
45 | 67 | signature = models.TextField() |
@@ -1,57 +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 |
private_key_str = private.to_ |
|
|
20 |
public_key_str = public.to_ |
|
|
19 | private_key_str = base64.b64encode(private.to_string()).decode() | |
|
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 | 28 | def verify(self, public_key_str, string, signature, key_type=TYPE_ECDSA): |
|
29 | 29 | if key_type == TYPE_ECDSA: |
|
30 |
public = VerifyingKey.from_ |
|
|
30 | public = VerifyingKey.from_string(base64.b64decode(public_key_str)) | |
|
31 | 31 | signature_byte = base64.b64decode(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 | def has_primary(self): | |
|
40 | return self.filter(primary=True).exists() | |
|
41 | ||
|
39 | 42 | |
|
40 | 43 | class KeyPair(models.Model): |
|
41 | 44 | class Meta: |
|
42 | 45 | app_label = APP_LABEL_BOARDS |
|
43 | 46 | |
|
44 | 47 | objects = KeyPairManager() |
|
45 | 48 | |
|
46 | 49 | public_key = models.TextField() |
|
47 | 50 | private_key = models.TextField() |
|
48 | 51 | key_type = models.TextField() |
|
49 | 52 | primary = models.BooleanField(default=False) |
|
50 | 53 | |
|
51 | 54 | def __str__(self): |
|
52 |
return '%s |
|
|
55 | return '%s | %s' % (self.key_type, self.public_key) | |
|
53 | 56 | |
|
54 | 57 | def sign(self, string): |
|
55 |
private = SigningKey.from_ |
|
|
58 | private = SigningKey.from_string(base64.b64decode( | |
|
59 | self.private_key.encode())) | |
|
56 | 60 | signature_byte = private.sign(string.encode()) |
|
57 | 61 | return base64.b64encode(signature_byte) |
@@ -1,103 +1,103 b'' | |||
|
1 | 1 | {% load i18n %} |
|
2 | 2 | {% load board %} |
|
3 | 3 | {% load cache %} |
|
4 | 4 | |
|
5 | 5 | {% get_current_language as LANGUAGE_CODE %} |
|
6 | 6 | |
|
7 | 7 | {% spaceless %} |
|
8 | 8 | {% cache 600 post post.id post.last_edit_time thread.archived bumpable truncated moderator LANGUAGE_CODE need_open_link %} |
|
9 | 9 | {% if thread.archived %} |
|
10 | 10 | <div class="post archive_post" id="{{ post.id }}"> |
|
11 | 11 | {% elif bumpable %} |
|
12 | 12 | <div class="post" id="{{ post.id }}"> |
|
13 | 13 | {% else %} |
|
14 | 14 | <div class="post dead_post" id="{{ post.id }}"> |
|
15 | 15 | {% endif %} |
|
16 | 16 | |
|
17 | 17 | <div class="post-info"> |
|
18 | 18 | <a class="post_id" href="{% post_object_url post thread=thread %}" |
|
19 | 19 | {% if not truncated and not thread.archived %} |
|
20 | 20 | onclick="javascript:addQuickReply('{{ post.id }}'); return false;" |
|
21 | 21 | title="{% trans 'Quote' %}" |
|
22 | 22 | {% endif %} |
|
23 | 23 | >({{ post.id }}) </a> |
|
24 | 24 | <span class="title">{{ post.title }} </span> |
|
25 | 25 | <span class="pub_time">{{ post.pub_time }}</span> |
|
26 | 26 | {% if thread.archived %} |
|
27 | 27 | — {{ thread.bump_time }} |
|
28 | 28 | {% endif %} |
|
29 | 29 | {% if is_opening and need_open_link %} |
|
30 | 30 | {% if thread.archived %} |
|
31 | 31 | [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>] |
|
32 | 32 | {% else %} |
|
33 | 33 | [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>] |
|
34 | 34 | {% endif %} |
|
35 | 35 | {% endif %} |
|
36 | 36 | |
|
37 | 37 | {% if post.global_id %} |
|
38 |
<span class="global-id"> |
|
|
38 | <span class="global-id"> {{ post.global_id }} </span> | |
|
39 | 39 | {% endif %} |
|
40 | 40 | |
|
41 | 41 | {% if moderator %} |
|
42 | 42 | <span class="moderator_info"> |
|
43 | 43 | [<a href="{% url 'post_admin' post_id=post.id %}" |
|
44 | 44 | >{% trans 'Edit' %}</a>] |
|
45 | 45 | [<a href="{% url 'delete' post_id=post.id %}" |
|
46 | 46 | >{% trans 'Delete' %}</a>] |
|
47 | 47 | ({{ post.poster_ip }}) |
|
48 | 48 | [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}" |
|
49 | 49 | >{% trans 'Ban IP' %}</a>] |
|
50 | 50 | </span> |
|
51 | 51 | {% endif %} |
|
52 | 52 | </div> |
|
53 | 53 | {% if post.images.exists %} |
|
54 | 54 | {% with post.images.all.0 as image %} |
|
55 | 55 | <div class="image"> |
|
56 | 56 | <a |
|
57 | 57 | class="thumb" |
|
58 | 58 | href="{{ image.image.url }}"><img |
|
59 | 59 | src="{{ image.image.url_200x150 }}" |
|
60 | 60 | alt="{{ post.id }}" |
|
61 | 61 | width="{{ image.pre_width }}" |
|
62 | 62 | height="{{ image.pre_height }}" |
|
63 | 63 | data-width="{{ image.width }}" |
|
64 | 64 | data-height="{{ image.height }}"/> |
|
65 | 65 | </a> |
|
66 | 66 | </div> |
|
67 | 67 | {% endwith %} |
|
68 | 68 | {% endif %} |
|
69 | 69 | <div class="message"> |
|
70 | 70 | {% autoescape off %} |
|
71 | 71 | {% if truncated %} |
|
72 | 72 | {{ post.text.rendered|truncatewords_html:50 }} |
|
73 | 73 | {% else %} |
|
74 | 74 | {{ post.text.rendered }} |
|
75 | 75 | {% endif %} |
|
76 | 76 | {% endautoescape %} |
|
77 | 77 | {% if post.is_referenced %} |
|
78 | 78 | <div class="refmap"> |
|
79 | 79 | {% autoescape off %} |
|
80 | 80 | {% trans "Replies" %}: {{ post.refmap }} |
|
81 | 81 | {% endautoescape %} |
|
82 | 82 | </div> |
|
83 | 83 | {% endif %} |
|
84 | 84 | </div> |
|
85 | 85 | {% endcache %} |
|
86 | 86 | {% if is_opening %} |
|
87 | 87 | {% cache 600 post_thread thread.id thread.last_edit_time LANGUAGE_CODE need_open_link %} |
|
88 | 88 | <div class="metadata"> |
|
89 | 89 | {% if is_opening and need_open_link %} |
|
90 | 90 | {{ thread.get_reply_count }} {% trans 'messages' %}, |
|
91 | 91 | {{ thread.get_images_count }} {% trans 'images' %}. |
|
92 | 92 | {% endif %} |
|
93 | 93 | <span class="tags"> |
|
94 | 94 | {% for tag in thread.get_tags %} |
|
95 | 95 | <a class="tag" href="{% url 'tag' tag.name %}"> |
|
96 | 96 | #{{ tag.name }}</a>{% if not forloop.last %},{% endif %} |
|
97 | 97 | {% endfor %} |
|
98 | 98 | </span> |
|
99 | 99 | </div> |
|
100 | 100 | {% endcache %} |
|
101 | 101 | {% endif %} |
|
102 | 102 | </div> |
|
103 | 103 | {% endspaceless %} |
@@ -1,6 +1,33 b'' | |||
|
1 | import xml.etree.ElementTree as et | |
|
2 | from django.http import HttpResponse | |
|
3 | from boards.models import GlobalId, Post | |
|
4 | ||
|
5 | ||
|
1 | 6 | def respond_pull(request): |
|
2 | 7 | pass |
|
3 | 8 | |
|
4 | 9 | |
|
5 | 10 | def respond_get(request): |
|
11 | """ | |
|
12 | Processes a GET request with post ID list and returns the posts XML list. | |
|
13 | Request should contain an 'xml' post attribute with the actual request XML. | |
|
14 | """ | |
|
15 | ||
|
16 | request_xml = request.POST['xml'] | |
|
17 | ||
|
18 | posts = [] | |
|
19 | ||
|
20 | root_tag = et.fromstring(request_xml) | |
|
21 | model_tag = root_tag[0] | |
|
22 | for id_tag in model_tag: | |
|
23 | try: | |
|
24 | global_id = GlobalId.from_xml_element(id_tag, existing=True) | |
|
25 | posts += Post.objects.filter(global_id=global_id) | |
|
26 | except GlobalId.DoesNotExist: | |
|
27 | # This is normal. If we don't have such GlobalId in the system, | |
|
28 | # just ignore this ID and proceed to the next one. | |
|
6 | 29 | pass |
|
30 | ||
|
31 | response_xml = Post.objects.generate_response_get(posts) | |
|
32 | ||
|
33 | return HttpResponse(content=response_xml) No newline at end of file |
General Comments 0
You need to be logged in to leave comments.
Login now