##// 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 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 / %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 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_pem().decode()
19 private_key_str = base64.b64encode(private.to_string()).decode()
20 public_key_str = public.to_pem().decode()
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_pem(public_key_str)
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: %s' % (self.key_type, self.public_key)
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_pem(self.private_key)
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">{{ 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 {% 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