diff --git a/boards/management/commands/generate_keypair.py b/boards/management/commands/generate_keypair.py new file mode 100644 --- /dev/null +++ b/boards/management/commands/generate_keypair.py @@ -0,0 +1,17 @@ +__author__ = 'neko259' + + +from django.core.management import BaseCommand +from django.db import transaction + +from boards.models import KeyPair + + +class Command(BaseCommand): + help = 'Generates the new keypair. The first one will be primary.' + + @transaction.atomic + def handle(self, *args, **options): + key = KeyPair.objects.generate_key( + primary=not KeyPair.objects.has_primary()) + print(key) \ No newline at end of file diff --git a/boards/models/signature.py b/boards/models/signature.py --- a/boards/models/signature.py +++ b/boards/models/signature.py @@ -24,9 +24,9 @@ class GlobalId(models.Model): local_id = models.IntegerField() def __str__(self): - return '%s / %s / %d' % (self.key_type, self.key, self.local_id) + return '%s | %s | %d' % (self.key_type, self.key, self.local_id) - def to_xml_element(self, element: et.SubElement): + def to_xml_element(self, element: et.Element): """ Exports global id to an XML element. """ @@ -35,6 +35,28 @@ class GlobalId(models.Model): element.set(ATTR_KEY_TYPE, self.key_type) element.set(ATTR_LOCAL_ID, str(self.local_id)) + @staticmethod + def from_xml_element(element: et.Element, existing=False): + """ + Parses XML id tag and gets global id from it. + + Arguments: + element -- the XML 'id' element + existing -- if this is False, a new instance of GlobalId will be + created. Otherwise, we will search for an existing GlobalId instance + and throw DoesNotExist if there isn't one. + """ + + if existing: + return GlobalId.objects.get(key=element.get(ATTR_KEY), + key_type=element.get(ATTR_KEY_TYPE), + local_id=int(element.get( + ATTR_LOCAL_ID))) + else: + return GlobalId(key=element.get(ATTR_KEY), + key_type=element.get(ATTR_KEY_TYPE), + local_id=int(element.get(ATTR_LOCAL_ID))) + class Signature(models.Model): class Meta: diff --git a/boards/models/sync_key.py b/boards/models/sync_key.py --- a/boards/models/sync_key.py +++ b/boards/models/sync_key.py @@ -16,8 +16,8 @@ class KeyPairManager(models.Manager): private = SigningKey.generate() public = private.get_verifying_key() - private_key_str = private.to_pem().decode() - public_key_str = public.to_pem().decode() + private_key_str = base64.b64encode(private.to_string()).decode() + public_key_str = base64.b64encode(public.to_string()).decode() return self.create(public_key=public_key_str, private_key=private_key_str, @@ -27,7 +27,7 @@ class KeyPairManager(models.Manager): def verify(self, public_key_str, string, signature, key_type=TYPE_ECDSA): if key_type == TYPE_ECDSA: - public = VerifyingKey.from_pem(public_key_str) + public = VerifyingKey.from_string(base64.b64decode(public_key_str)) signature_byte = base64.b64decode(signature) try: return public.verify(signature_byte, string.encode()) @@ -36,6 +36,9 @@ class KeyPairManager(models.Manager): else: raise Exception('Key type not supported') + def has_primary(self): + return self.filter(primary=True).exists() + class KeyPair(models.Model): class Meta: @@ -49,9 +52,10 @@ class KeyPair(models.Model): primary = models.BooleanField(default=False) def __str__(self): - return '%s: %s' % (self.key_type, self.public_key) + return '%s | %s' % (self.key_type, self.public_key) def sign(self, string): - private = SigningKey.from_pem(self.private_key) + private = SigningKey.from_string(base64.b64decode( + self.private_key.encode())) signature_byte = private.sign(string.encode()) return base64.b64encode(signature_byte) diff --git a/boards/templates/boards/post.html b/boards/templates/boards/post.html --- a/boards/templates/boards/post.html +++ b/boards/templates/boards/post.html @@ -35,7 +35,7 @@ {% endif %} {% if post.global_id %} - {{ post.global_id.key_type }} / {{ post.global_id.key }} / {{ post.global_id.local_id }} + {{ post.global_id }} {% endif %} {% if moderator %} diff --git a/boards/tests/test_sync.py b/boards/tests/test_sync.py new file mode 100644 --- /dev/null +++ b/boards/tests/test_sync.py @@ -0,0 +1,55 @@ +from boards.models import KeyPair, Post +from boards.tests.mocks import MockRequest +from boards.views.sync import respond_get + +__author__ = 'neko259' + + +from django.test import TestCase + + +class SyncTest(TestCase): + def test_get(self): + """ + Forms a GET request of a post and checks the response. + """ + + key = KeyPair(public_key='pubkey', private_key='privkey', + key_type='test_key_type', primary=True) + key.save() + + post = Post.objects.create_post(title='test_title', text='test_text') + + request = MockRequest() + request.POST['xml'] = ( + '' + '' + '' + '' + '' % (post.global_id.key, + post.id, + post.global_id.key_type) + ) + + self.assertTrue( + '' + 'success' + '' + '' + '' + '%s' + '%s' + '%d' + '%d' + '' + '' + '' % ( + post.global_id.key, + post.id, + post.global_id.key_type, + post.title, + post.text.raw, + post.get_pub_time_epoch(), + post.get_edit_time_epoch(), + ) in respond_get(request).content.decode(), + 'Wrong response generated for the GET request.') \ No newline at end of file diff --git a/boards/views/sync.py b/boards/views/sync.py --- a/boards/views/sync.py +++ b/boards/views/sync.py @@ -1,6 +1,33 @@ +import xml.etree.ElementTree as et +from django.http import HttpResponse +from boards.models import GlobalId, Post + + def respond_pull(request): pass def respond_get(request): - pass + """ + Processes a GET request with post ID list and returns the posts XML list. + Request should contain an 'xml' post attribute with the actual request XML. + """ + + request_xml = request.POST['xml'] + + posts = [] + + root_tag = et.fromstring(request_xml) + model_tag = root_tag[0] + for id_tag in model_tag: + try: + global_id = GlobalId.from_xml_element(id_tag, existing=True) + posts += Post.objects.filter(global_id=global_id) + except GlobalId.DoesNotExist: + # This is normal. If we don't have such GlobalId in the system, + # just ignore this ID and proceed to the next one. + pass + + response_xml = Post.objects.generate_response_get(posts) + + return HttpResponse(content=response_xml) \ No newline at end of file