##// END OF EJS Templates
Added GET request handler, command for key generation. Changed KeyPair format for ecdsa (use to_string instead of to_pem because it's shorter)
neko259 -
r836:9ee107b9 decentral
parent child Browse files
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 / %s / %d' % (self.key_type, self.key, self.local_id)
27 return '%s | %s | %d' % (self.key_type, self.key, self.local_id)
28 28
29 def to_xml_element(self, element: et.SubElement):
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_pem().decode()
20 public_key_str = public.to_pem().decode()
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_pem(public_key_str)
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: %s' % (self.key_type, self.public_key)
55 return '%s | %s' % (self.key_type, self.public_key)
53 56
54 57 def sign(self, string):
55 private = SigningKey.from_pem(self.private_key)
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">{{ post.global_id.key_type }} / {{ post.global_id.key }} / {{ post.global_id.local_id }}</span>
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