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 | import xml.etree.ElementTree as et |
|
1 | import xml.etree.ElementTree as et | |
2 | from django.db import models |
|
2 | from django.db import models | |
3 |
|
3 | |||
4 |
|
4 | |||
5 | ATTR_KEY = 'key' |
|
5 | ATTR_KEY = 'key' | |
6 | ATTR_KEY_TYPE = 'type' |
|
6 | ATTR_KEY_TYPE = 'type' | |
7 | ATTR_LOCAL_ID = 'local-id' |
|
7 | ATTR_LOCAL_ID = 'local-id' | |
8 |
|
8 | |||
9 |
|
9 | |||
10 | class GlobalId(models.Model): |
|
10 | class GlobalId(models.Model): | |
11 | class Meta: |
|
11 | class Meta: | |
12 | app_label = 'boards' |
|
12 | app_label = 'boards' | |
13 |
|
13 | |||
14 | def __init__(self, *args, **kwargs): |
|
14 | def __init__(self, *args, **kwargs): | |
15 | models.Model.__init__(self, *args, **kwargs) |
|
15 | models.Model.__init__(self, *args, **kwargs) | |
16 |
|
16 | |||
17 | if 'key' in kwargs and 'key_type' in kwargs and 'local_id' in kwargs: |
|
17 | if 'key' in kwargs and 'key_type' in kwargs and 'local_id' in kwargs: | |
18 | self.key = kwargs['key'] |
|
18 | self.key = kwargs['key'] | |
19 | self.key_type = kwargs['key_type'] |
|
19 | self.key_type = kwargs['key_type'] | |
20 | self.local_id = kwargs['local_id'] |
|
20 | self.local_id = kwargs['local_id'] | |
21 |
|
21 | |||
22 | key = models.TextField() |
|
22 | key = models.TextField() | |
23 | key_type = models.TextField() |
|
23 | key_type = models.TextField() | |
24 | local_id = models.IntegerField() |
|
24 | local_id = models.IntegerField() | |
25 |
|
25 | |||
26 | def __str__(self): |
|
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 | Exports global id to an XML element. |
|
31 | Exports global id to an XML element. | |
32 | """ |
|
32 | """ | |
33 |
|
33 | |||
34 | element.set(ATTR_KEY, self.key) |
|
34 | element.set(ATTR_KEY, self.key) | |
35 | element.set(ATTR_KEY_TYPE, self.key_type) |
|
35 | element.set(ATTR_KEY_TYPE, self.key_type) | |
36 | element.set(ATTR_LOCAL_ID, str(self.local_id)) |
|
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 | class Signature(models.Model): |
|
61 | class Signature(models.Model): | |
40 | class Meta: |
|
62 | class Meta: | |
41 | app_label = 'boards' |
|
63 | app_label = 'boards' | |
42 |
|
64 | |||
43 | key_type = models.TextField() |
|
65 | key_type = models.TextField() | |
44 | key = models.TextField() |
|
66 | key = models.TextField() | |
45 | signature = models.TextField() |
|
67 | signature = models.TextField() |
@@ -1,57 +1,61 b'' | |||||
1 | import base64 |
|
1 | import base64 | |
2 | from ecdsa import SigningKey, VerifyingKey, BadSignatureError |
|
2 | from ecdsa import SigningKey, VerifyingKey, BadSignatureError | |
3 | from django.db import models |
|
3 | from django.db import models | |
4 |
|
4 | |||
5 | TYPE_ECDSA = 'ecdsa' |
|
5 | TYPE_ECDSA = 'ecdsa' | |
6 |
|
6 | |||
7 | APP_LABEL_BOARDS = 'boards' |
|
7 | APP_LABEL_BOARDS = 'boards' | |
8 |
|
8 | |||
9 |
|
9 | |||
10 | class KeyPairManager(models.Manager): |
|
10 | class KeyPairManager(models.Manager): | |
11 | def generate_key(self, key_type=TYPE_ECDSA, primary=False): |
|
11 | def generate_key(self, key_type=TYPE_ECDSA, primary=False): | |
12 | if primary and self.filter(primary=True).exists(): |
|
12 | if primary and self.filter(primary=True).exists(): | |
13 | raise Exception('There can be only one primary key') |
|
13 | raise Exception('There can be only one primary key') | |
14 |
|
14 | |||
15 | if key_type == TYPE_ECDSA: |
|
15 | if key_type == TYPE_ECDSA: | |
16 | private = SigningKey.generate() |
|
16 | private = SigningKey.generate() | |
17 | public = private.get_verifying_key() |
|
17 | public = private.get_verifying_key() | |
18 |
|
18 | |||
19 |
private_key_str = private.to_ |
|
19 | private_key_str = base64.b64encode(private.to_string()).decode() | |
20 |
public_key_str = public.to_ |
|
20 | public_key_str = base64.b64encode(public.to_string()).decode() | |
21 |
|
21 | |||
22 | return self.create(public_key=public_key_str, |
|
22 | return self.create(public_key=public_key_str, | |
23 | private_key=private_key_str, |
|
23 | private_key=private_key_str, | |
24 | key_type=TYPE_ECDSA, primary=primary) |
|
24 | key_type=TYPE_ECDSA, primary=primary) | |
25 | else: |
|
25 | else: | |
26 | raise Exception('Key type not supported') |
|
26 | raise Exception('Key type not supported') | |
27 |
|
27 | |||
28 | def verify(self, public_key_str, string, signature, key_type=TYPE_ECDSA): |
|
28 | def verify(self, public_key_str, string, signature, key_type=TYPE_ECDSA): | |
29 | if key_type == TYPE_ECDSA: |
|
29 | if key_type == TYPE_ECDSA: | |
30 |
public = VerifyingKey.from_ |
|
30 | public = VerifyingKey.from_string(base64.b64decode(public_key_str)) | |
31 | signature_byte = base64.b64decode(signature) |
|
31 | signature_byte = base64.b64decode(signature) | |
32 | try: |
|
32 | try: | |
33 | return public.verify(signature_byte, string.encode()) |
|
33 | return public.verify(signature_byte, string.encode()) | |
34 | except BadSignatureError: |
|
34 | except BadSignatureError: | |
35 | return False |
|
35 | return False | |
36 | else: |
|
36 | else: | |
37 | raise Exception('Key type not supported') |
|
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 | class KeyPair(models.Model): |
|
43 | class KeyPair(models.Model): | |
41 | class Meta: |
|
44 | class Meta: | |
42 | app_label = APP_LABEL_BOARDS |
|
45 | app_label = APP_LABEL_BOARDS | |
43 |
|
46 | |||
44 | objects = KeyPairManager() |
|
47 | objects = KeyPairManager() | |
45 |
|
48 | |||
46 | public_key = models.TextField() |
|
49 | public_key = models.TextField() | |
47 | private_key = models.TextField() |
|
50 | private_key = models.TextField() | |
48 | key_type = models.TextField() |
|
51 | key_type = models.TextField() | |
49 | primary = models.BooleanField(default=False) |
|
52 | primary = models.BooleanField(default=False) | |
50 |
|
53 | |||
51 | def __str__(self): |
|
54 | def __str__(self): | |
52 |
return '%s |
|
55 | return '%s | %s' % (self.key_type, self.public_key) | |
53 |
|
56 | |||
54 | def sign(self, string): |
|
57 | def sign(self, string): | |
55 |
private = SigningKey.from_ |
|
58 | private = SigningKey.from_string(base64.b64decode( | |
|
59 | self.private_key.encode())) | |||
56 | signature_byte = private.sign(string.encode()) |
|
60 | signature_byte = private.sign(string.encode()) | |
57 | return base64.b64encode(signature_byte) |
|
61 | return base64.b64encode(signature_byte) |
@@ -1,103 +1,103 b'' | |||||
1 | {% load i18n %} |
|
1 | {% load i18n %} | |
2 | {% load board %} |
|
2 | {% load board %} | |
3 | {% load cache %} |
|
3 | {% load cache %} | |
4 |
|
4 | |||
5 | {% get_current_language as LANGUAGE_CODE %} |
|
5 | {% get_current_language as LANGUAGE_CODE %} | |
6 |
|
6 | |||
7 | {% spaceless %} |
|
7 | {% spaceless %} | |
8 | {% cache 600 post post.id post.last_edit_time thread.archived bumpable truncated moderator LANGUAGE_CODE need_open_link %} |
|
8 | {% cache 600 post post.id post.last_edit_time thread.archived bumpable truncated moderator LANGUAGE_CODE need_open_link %} | |
9 | {% if thread.archived %} |
|
9 | {% if thread.archived %} | |
10 | <div class="post archive_post" id="{{ post.id }}"> |
|
10 | <div class="post archive_post" id="{{ post.id }}"> | |
11 | {% elif bumpable %} |
|
11 | {% elif bumpable %} | |
12 | <div class="post" id="{{ post.id }}"> |
|
12 | <div class="post" id="{{ post.id }}"> | |
13 | {% else %} |
|
13 | {% else %} | |
14 | <div class="post dead_post" id="{{ post.id }}"> |
|
14 | <div class="post dead_post" id="{{ post.id }}"> | |
15 | {% endif %} |
|
15 | {% endif %} | |
16 |
|
16 | |||
17 | <div class="post-info"> |
|
17 | <div class="post-info"> | |
18 | <a class="post_id" href="{% post_object_url post thread=thread %}" |
|
18 | <a class="post_id" href="{% post_object_url post thread=thread %}" | |
19 | {% if not truncated and not thread.archived %} |
|
19 | {% if not truncated and not thread.archived %} | |
20 | onclick="javascript:addQuickReply('{{ post.id }}'); return false;" |
|
20 | onclick="javascript:addQuickReply('{{ post.id }}'); return false;" | |
21 | title="{% trans 'Quote' %}" |
|
21 | title="{% trans 'Quote' %}" | |
22 | {% endif %} |
|
22 | {% endif %} | |
23 | >({{ post.id }}) </a> |
|
23 | >({{ post.id }}) </a> | |
24 | <span class="title">{{ post.title }} </span> |
|
24 | <span class="title">{{ post.title }} </span> | |
25 | <span class="pub_time">{{ post.pub_time }}</span> |
|
25 | <span class="pub_time">{{ post.pub_time }}</span> | |
26 | {% if thread.archived %} |
|
26 | {% if thread.archived %} | |
27 | β {{ thread.bump_time }} |
|
27 | β {{ thread.bump_time }} | |
28 | {% endif %} |
|
28 | {% endif %} | |
29 | {% if is_opening and need_open_link %} |
|
29 | {% if is_opening and need_open_link %} | |
30 | {% if thread.archived %} |
|
30 | {% if thread.archived %} | |
31 | [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>] |
|
31 | [<a class="link" href="{% url 'thread' post.id %}">{% trans "Open" %}</a>] | |
32 | {% else %} |
|
32 | {% else %} | |
33 | [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>] |
|
33 | [<a class="link" href="{% url 'thread' post.id %}#form">{% trans "Reply" %}</a>] | |
34 | {% endif %} |
|
34 | {% endif %} | |
35 | {% endif %} |
|
35 | {% endif %} | |
36 |
|
36 | |||
37 | {% if post.global_id %} |
|
37 | {% if post.global_id %} | |
38 |
<span class="global-id"> |
|
38 | <span class="global-id"> {{ post.global_id }} </span> | |
39 | {% endif %} |
|
39 | {% endif %} | |
40 |
|
40 | |||
41 | {% if moderator %} |
|
41 | {% if moderator %} | |
42 | <span class="moderator_info"> |
|
42 | <span class="moderator_info"> | |
43 | [<a href="{% url 'post_admin' post_id=post.id %}" |
|
43 | [<a href="{% url 'post_admin' post_id=post.id %}" | |
44 | >{% trans 'Edit' %}</a>] |
|
44 | >{% trans 'Edit' %}</a>] | |
45 | [<a href="{% url 'delete' post_id=post.id %}" |
|
45 | [<a href="{% url 'delete' post_id=post.id %}" | |
46 | >{% trans 'Delete' %}</a>] |
|
46 | >{% trans 'Delete' %}</a>] | |
47 | ({{ post.poster_ip }}) |
|
47 | ({{ post.poster_ip }}) | |
48 | [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}" |
|
48 | [<a href="{% url 'ban' post_id=post.id %}?next={{ request.path }}" | |
49 | >{% trans 'Ban IP' %}</a>] |
|
49 | >{% trans 'Ban IP' %}</a>] | |
50 | </span> |
|
50 | </span> | |
51 | {% endif %} |
|
51 | {% endif %} | |
52 | </div> |
|
52 | </div> | |
53 | {% if post.images.exists %} |
|
53 | {% if post.images.exists %} | |
54 | {% with post.images.all.0 as image %} |
|
54 | {% with post.images.all.0 as image %} | |
55 | <div class="image"> |
|
55 | <div class="image"> | |
56 | <a |
|
56 | <a | |
57 | class="thumb" |
|
57 | class="thumb" | |
58 | href="{{ image.image.url }}"><img |
|
58 | href="{{ image.image.url }}"><img | |
59 | src="{{ image.image.url_200x150 }}" |
|
59 | src="{{ image.image.url_200x150 }}" | |
60 | alt="{{ post.id }}" |
|
60 | alt="{{ post.id }}" | |
61 | width="{{ image.pre_width }}" |
|
61 | width="{{ image.pre_width }}" | |
62 | height="{{ image.pre_height }}" |
|
62 | height="{{ image.pre_height }}" | |
63 | data-width="{{ image.width }}" |
|
63 | data-width="{{ image.width }}" | |
64 | data-height="{{ image.height }}"/> |
|
64 | data-height="{{ image.height }}"/> | |
65 | </a> |
|
65 | </a> | |
66 | </div> |
|
66 | </div> | |
67 | {% endwith %} |
|
67 | {% endwith %} | |
68 | {% endif %} |
|
68 | {% endif %} | |
69 | <div class="message"> |
|
69 | <div class="message"> | |
70 | {% autoescape off %} |
|
70 | {% autoescape off %} | |
71 | {% if truncated %} |
|
71 | {% if truncated %} | |
72 | {{ post.text.rendered|truncatewords_html:50 }} |
|
72 | {{ post.text.rendered|truncatewords_html:50 }} | |
73 | {% else %} |
|
73 | {% else %} | |
74 | {{ post.text.rendered }} |
|
74 | {{ post.text.rendered }} | |
75 | {% endif %} |
|
75 | {% endif %} | |
76 | {% endautoescape %} |
|
76 | {% endautoescape %} | |
77 | {% if post.is_referenced %} |
|
77 | {% if post.is_referenced %} | |
78 | <div class="refmap"> |
|
78 | <div class="refmap"> | |
79 | {% autoescape off %} |
|
79 | {% autoescape off %} | |
80 | {% trans "Replies" %}: {{ post.refmap }} |
|
80 | {% trans "Replies" %}: {{ post.refmap }} | |
81 | {% endautoescape %} |
|
81 | {% endautoescape %} | |
82 | </div> |
|
82 | </div> | |
83 | {% endif %} |
|
83 | {% endif %} | |
84 | </div> |
|
84 | </div> | |
85 | {% endcache %} |
|
85 | {% endcache %} | |
86 | {% if is_opening %} |
|
86 | {% if is_opening %} | |
87 | {% cache 600 post_thread thread.id thread.last_edit_time LANGUAGE_CODE need_open_link %} |
|
87 | {% cache 600 post_thread thread.id thread.last_edit_time LANGUAGE_CODE need_open_link %} | |
88 | <div class="metadata"> |
|
88 | <div class="metadata"> | |
89 | {% if is_opening and need_open_link %} |
|
89 | {% if is_opening and need_open_link %} | |
90 | {{ thread.get_reply_count }} {% trans 'messages' %}, |
|
90 | {{ thread.get_reply_count }} {% trans 'messages' %}, | |
91 | {{ thread.get_images_count }} {% trans 'images' %}. |
|
91 | {{ thread.get_images_count }} {% trans 'images' %}. | |
92 | {% endif %} |
|
92 | {% endif %} | |
93 | <span class="tags"> |
|
93 | <span class="tags"> | |
94 | {% for tag in thread.get_tags %} |
|
94 | {% for tag in thread.get_tags %} | |
95 | <a class="tag" href="{% url 'tag' tag.name %}"> |
|
95 | <a class="tag" href="{% url 'tag' tag.name %}"> | |
96 | #{{ tag.name }}</a>{% if not forloop.last %},{% endif %} |
|
96 | #{{ tag.name }}</a>{% if not forloop.last %},{% endif %} | |
97 | {% endfor %} |
|
97 | {% endfor %} | |
98 | </span> |
|
98 | </span> | |
99 | </div> |
|
99 | </div> | |
100 | {% endcache %} |
|
100 | {% endcache %} | |
101 | {% endif %} |
|
101 | {% endif %} | |
102 | </div> |
|
102 | </div> | |
103 | {% endspaceless %} |
|
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 | def respond_pull(request): |
|
6 | def respond_pull(request): | |
2 | pass |
|
7 | pass | |
3 |
|
8 | |||
4 |
|
9 | |||
5 | def respond_get(request): |
|
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 | pass |
|
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