From ed24c2140a0719b8cbda3824c185184f8fce1b31 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 5 Aug 2020 11:28:54 +0100 Subject: [PATCH 01/34] Actor context to support devices --- person.py | 52 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/person.py b/person.py index 78b20a01e..7dd1af992 100644 --- a/person.py +++ b/person.py @@ -163,6 +163,36 @@ def randomizeActorImages(personJson: {}) -> None: personId + '/image' + randStr + '.' + existingExtension +def getDefaultPersonContext() -> str: + return { + 'Emoji': 'toot:Emoji', + 'Hashtag': 'as:Hashtag', + 'IdentityProof': 'toot:IdentityProof', + 'PropertyValue': 'schema:PropertyValue', + 'alsoKnownAs': { + '@id': 'as:alsoKnownAs', '@type': '@id' + }, + 'focalPoint': { + '@container': '@list', '@id': 'toot:focalPoint' + }, + 'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers', + 'movedTo': { + '@id': 'as:movedTo', '@type': '@id' + }, + 'schema': 'http://schema.org#', + 'value': 'schema:value', + 'Curve25519Key': 'toot:Curve25519Key', + 'Device': 'toot:Device', + 'Ed25519Key': 'toot:Ed25519Key', + 'Ed25519Signature': 'toot:Ed25519Signature', + 'EncryptedMessage': 'toot:EncryptedMessage', + 'identityKey': {'@id': 'toot:identityKey', '@type': '@id'}, + 'fingerprintKey': {'@id': 'toot:fingerprintKey', '@type': '@id'}, + 'messageFranking': 'toot:messageFranking', + 'publicKeyBase64': 'toot:publicKeyBase64' + } + + def createPersonBase(baseDir: str, nickname: str, domain: str, port: int, httpPrefix: str, saveToFile: bool, manualFollowerApproval: bool, @@ -212,34 +242,16 @@ def createPersonBase(baseDir: str, nickname: str, domain: str, port: int, personId + '/avatar' + \ str(randint(10000000000000, 99999999999999)) + '.png' # nosec - contextDict = { - 'Emoji': 'toot:Emoji', - 'Hashtag': 'as:Hashtag', - 'IdentityProof': 'toot:IdentityProof', - 'PropertyValue': 'schema:PropertyValue', - 'alsoKnownAs': { - '@id': 'as:alsoKnownAs', '@type': '@id' - }, - 'focalPoint': { - '@container': '@list', '@id': 'toot:focalPoint' - }, - 'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers', - 'movedTo': { - '@id': 'as:movedTo', '@type': '@id' - }, - 'schema': 'http://schema.org#', - 'value': 'schema:value' - } - newPerson = { '@context': [ 'https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', - contextDict + getDefaultPersonContext() ], 'attachment': [], 'alsoKnownAs': [], 'discoverable': False, + 'devices': personId + '/collections/devices', 'endpoints': { 'id': personId+'/endpoints', 'sharedInbox': httpPrefix+'://'+domain+'/inbox', From c682c759ba873e1c803eae92a0338e7ab2d7e170 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 5 Aug 2020 11:33:47 +0100 Subject: [PATCH 02/34] Update actor context when saved --- daemon.py | 7 +++++++ person.py | 2 ++ 2 files changed, 9 insertions(+) diff --git a/daemon.py b/daemon.py index 864f69f17..cf92a69e9 100644 --- a/daemon.py +++ b/daemon.py @@ -43,6 +43,7 @@ from matrix import getMatrixAddress from matrix import setMatrixAddress from donate import getDonationUrl from donate import setDonationUrl +from person import getDefaultPersonContext from person import savePersonQrcode from person import randomizeActorImages from person import personUpgradeActor @@ -6616,6 +6617,12 @@ class PubServer(BaseHTTPRequestHandler): os.remove(gitProjectsFilename) # save actor json file within accounts if actorChanged: + # update the context for the actor + actorJson['@context'] = [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + getDefaultPersonContext() + ] randomizeActorImages(actorJson) saveJson(actorJson, actorFilename) webfingerUpdate(self.server.baseDir, diff --git a/person.py b/person.py index 7dd1af992..09fe26570 100644 --- a/person.py +++ b/person.py @@ -164,6 +164,8 @@ def randomizeActorImages(personJson: {}) -> None: def getDefaultPersonContext() -> str: + """Gets the default actor context + """ return { 'Emoji': 'toot:Emoji', 'Hashtag': 'as:Hashtag', From a5bcf1a35691f54a098dc0860f584c4d528d3d40 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 5 Aug 2020 13:05:39 +0100 Subject: [PATCH 03/34] Devices endpoint --- daemon.py | 19 +++++++++++++++++++ devices.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 devices.py diff --git a/daemon.py b/daemon.py index cf92a69e9..a02577f4c 100644 --- a/daemon.py +++ b/daemon.py @@ -192,6 +192,7 @@ from bookmarks import undoBookmark from petnames import setPetName from followingCalendar import addPersonToCalendar from followingCalendar import removePersonFromCalendar +from devices import devicesCollection import os @@ -1536,6 +1537,24 @@ class PubServer(BaseHTTPRequestHandler): self._404() return + # list of registered devices for e2ee + # see https://github.com/tootsuite/mastodon/pull/13820 + if not htmlGET and authorized and '/users/' in self.path: + if self.path.endswith('/collections/devices'): + nickname = self.path.split('/users/') + if '/' in nickname: + nickname = nickname.split('/')[0] + devJson = devicesCollection(self.server.baseDir, + nickname, self.server.domain, + self.server.domainFull, + self.server.httpPrefix) + msg = json.dumps(devJson, + ensure_ascii=False).encode('utf-8') + self._set_headers('application/json', + len(msg), + None, callingDomain) + self._write(msg) + if htmlGET and '/users/' in self.path: # show the person options screen with view/follow/block/report if '?options=' in self.path: diff --git a/devices.py b/devices.py new file mode 100644 index 000000000..607b6ee36 --- /dev/null +++ b/devices.py @@ -0,0 +1,39 @@ +__filename__ = "devices.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.1.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + +import os +from utils import loadJson + + +def devicesCollection(baseDir: str, nickname: str, domain: str, + domainFull: str, httpPrefix: str) -> {}: + """Returns a list of registered devices + """ + personDir = baseDir + '/accounts/' + nickname + '@' + domain + if not os.path.isdir(personDir): + return {} + personId = httpPrefix + '://' + domainFull + '/users/' + nickname + if not os.path.isdir(personDir + '/devices'): + os.mkdir(personDir + '/devices') + deviceList = [] + for subdir, dirs, files in os.walk(personDir + '/devices/'): + for dev in files: + if not dev.endswith('.json'): + continue + deviceFilename = os.path.join(personDir + '/devices', dev) + devJson = loadJson(deviceFilename) + if devJson: + deviceList.append(devJson) + + devicesDict = { + 'id': personId + '/collections/devices', + 'type': 'Collection', + 'totalItems': len(deviceList), + 'items': deviceList + } + return devicesDict From b554256a1fc676abfaeeabcac23aa95d50e36cf5 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 5 Aug 2020 13:07:00 +0100 Subject: [PATCH 04/34] Don't check for http --- daemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon.py b/daemon.py index a02577f4c..f77403c0d 100644 --- a/daemon.py +++ b/daemon.py @@ -1539,7 +1539,7 @@ class PubServer(BaseHTTPRequestHandler): # list of registered devices for e2ee # see https://github.com/tootsuite/mastodon/pull/13820 - if not htmlGET and authorized and '/users/' in self.path: + if authorized and '/users/' in self.path: if self.path.endswith('/collections/devices'): nickname = self.path.split('/users/') if '/' in nickname: From 0f075f37e97f6f943676c5cfe8ca57027d319da7 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 5 Aug 2020 13:16:15 +0100 Subject: [PATCH 05/34] Encrypted messages treated as DMs --- posts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/posts.py b/posts.py index f2aa61f43..df13d5790 100644 --- a/posts.py +++ b/posts.py @@ -2400,6 +2400,7 @@ def isDM(postJsonObject: {}) -> bool: return False if postJsonObject['object']['type'] != 'Note' and \ postJsonObject['object']['type'] != 'Patch' and \ + postJsonObject['object']['type'] != 'EncryptedMessage' and \ postJsonObject['object']['type'] != 'Article': return False if postJsonObject['object'].get('moderationStatus'): From 021ce1355cc5d3bc9fd231902f6985a092fecb5f Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 5 Aug 2020 13:24:09 +0100 Subject: [PATCH 06/34] EncryptedMessage type --- outbox.py | 1 + posts.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/outbox.py b/outbox.py index c84e69dcd..bbf863f22 100644 --- a/outbox.py +++ b/outbox.py @@ -189,6 +189,7 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str, if messageJson['type'] == 'Create' or \ messageJson['type'] == 'Question' or \ messageJson['type'] == 'Note' or \ + messageJson['type'] == 'EncryptedMessage' or \ messageJson['type'] == 'Article' or \ messageJson['type'] == 'Patch' or \ messageJson['type'] == 'Announce': diff --git a/posts.py b/posts.py index df13d5790..1aa7f8b14 100644 --- a/posts.py +++ b/posts.py @@ -2467,6 +2467,7 @@ def isReply(postJsonObject: {}, actor: str) -> bool: if postJsonObject['object'].get('moderationStatus'): return False if postJsonObject['object']['type'] != 'Note' and \ + postJsonObject['object']['type'] != 'EncryptedMessage' and \ postJsonObject['object']['type'] != 'Article': return False if postJsonObject['object'].get('inReplyTo'): @@ -2578,6 +2579,7 @@ def addPostStringToTimeline(postStr: str, boxname: str, """ # must be a recognized ActivityPub type if ('"Note"' in postStr or + '"EncryptedMessage"' in postStr or '"Article"' in postStr or '"Patch"' in postStr or '"Announce"' in postStr or From 0c90cde8f3a3b2e94cf7c6e00b80783dfed33244 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 5 Aug 2020 13:47:15 +0100 Subject: [PATCH 07/34] Local decrypt dummy function --- devices.py | 29 +++++++++++++++++++++++++++++ webinterface.py | 6 ++++++ 2 files changed, 35 insertions(+) diff --git a/devices.py b/devices.py index 607b6ee36..9fd97b45f 100644 --- a/devices.py +++ b/devices.py @@ -37,3 +37,32 @@ def devicesCollection(baseDir: str, nickname: str, domain: str, 'items': deviceList } return devicesDict + + +def decryptMessageFromDevice(messageJson: {}) -> str: + """Locally decrypts a message on the device. + This should probably be a link to a local script + or native app, such that what the user sees isn't + something which the server could get access to. + """ + # TODO + # { + # "type": "EncryptedMessage", + # "messageType": 0, + # "cipherText": "...", + # "digest": { + # "type": "Digest", + # "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#hmac-sha256", + # "digestValue": "5f6ad31acd64995483d75c7..." + # }, + # "messageFranking": "...", + # "attributedTo": { + # "type": "Device", + # "deviceId": "11119" + # }, + # "to": { + # "type": "Device", + # "deviceId": "11876" + # } + # } + return '' diff --git a/webinterface.py b/webinterface.py index aade59c81..45df3d27c 100644 --- a/webinterface.py +++ b/webinterface.py @@ -78,6 +78,7 @@ from git import isGitPatch from theme import getThemesList from petnames import getPetName from followingCalendar import receivingCalendarEvents +from devices import decryptMessageFromDevice def getAltPath(actor: str, domainFull: str, callingDomain: str) -> str: @@ -4248,8 +4249,13 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int, if not postJsonObject['object'].get('summary'): postJsonObject['object']['summary'] = '' + if postJsonObject['object'].get('cipherText'): + postJsonObject['object']['content'] = \ + decryptMessageFromDevice(postJsonObject['object']) + if not postJsonObject['object'].get('content'): return '' + isPatch = isGitPatch(baseDir, nickname, domain, postJsonObject['object']['type'], postJsonObject['object']['summary'], From 33bbe7194001d930bc094f3b0df59913dc8f43cd Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 5 Aug 2020 14:06:04 +0000 Subject: [PATCH 08/34] Add and remove device functions --- devices.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/devices.py b/devices.py index 9fd97b45f..81e74a4fe 100644 --- a/devices.py +++ b/devices.py @@ -8,6 +8,56 @@ __status__ = "Production" import os from utils import loadJson +from utils import saveJson + + +def removeDevice(baseDir: str, nickname: str, domain: str, + deviceId: str) -> bool: + """Unregisters a device for e2ee + """ + personDir = baseDir + '/accounts/' + nickname + '@' + domain + deviceFilename = personDir + '/devices/' + deviceId + '.json' + if os.path.isfile(deviceFilename): + os.remove(deviceFilename) + return True + return False + + +def addDevice(baseDir: str, nickname: str, domain: str, + deviceId: str, name: str, claimUrl: str, + fingerprintPublicKey: str, + identityPublicKey: str, + fingerprintKeyType="Ed25519Key", + identityKeyType="Curve25519Key") -> bool: + """Registers a device for e2ee + claimUrl could be something like: + http://localhost:3000/users/admin/claim?id=11119 + """ + if ' ' in deviceId or '/' in deviceId or \ + '?' in deviceId or '#' in deviceId or \ + '.' in deviceId: + return False + personDir = baseDir + '/accounts/' + nickname + '@' + domain + if not os.path.isdir(personDir): + return False + if not os.path.isdir(personDir + '/devices'): + os.mkdir(personDir + '/devices') + deviceDict = { + "deviceId": deviceId, + "type": "Device", + "name": name, + "claim": claimUrl, + "fingerprintKey": { + "type": fingerprintKeyType, + "publicKeyBase64": fingerprintPublicKey + }, + "identityKey": { + "type": identityKeyType, + "publicKeyBase64": identityPublicKey + } + } + deviceFilename = personDir + '/devices/' + deviceId + '.json' + return saveJson(deviceDict, deviceFilename) def devicesCollection(baseDir: str, nickname: str, domain: str, From a14d6a014e8db75dfdbeea8dc2232b9e14709bd1 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 5 Aug 2020 22:12:09 +0100 Subject: [PATCH 09/34] Add notes about people --- daemon.py | 24 ++++++++++++++++++++++++ person.py | 15 +++++++++++++++ translations/ar.json | 3 ++- translations/ca.json | 3 ++- translations/cy.json | 3 ++- translations/de.json | 3 ++- translations/en.json | 3 ++- translations/es.json | 3 ++- translations/fr.json | 3 ++- translations/ga.json | 3 ++- translations/hi.json | 3 ++- translations/it.json | 3 ++- translations/ja.json | 3 ++- translations/oc.json | 3 ++- translations/pt.json | 3 ++- translations/ru.json | 3 ++- translations/zh.json | 3 ++- webinterface.py | 22 ++++++++++++++++++++-- 18 files changed, 89 insertions(+), 17 deletions(-) diff --git a/daemon.py b/daemon.py index f77403c0d..c7f720af8 100644 --- a/daemon.py +++ b/daemon.py @@ -43,6 +43,7 @@ from matrix import getMatrixAddress from matrix import setMatrixAddress from donate import getDonationUrl from donate import setDonationUrl +from person import setPersonNotes from person import getDefaultPersonContext from person import savePersonQrcode from person import randomizeActorImages @@ -7757,6 +7758,15 @@ class PubServer(BaseHTTPRequestHandler): '?' in petname or '#' in petname: petname = None + personNotes = None + if 'optionnotes' in optionsConfirmParams: + personNotes = optionsConfirmParams.split('optionnotes=')[1] + if '&' in personNotes: + personNotes = personNotes.split('&')[0] + # Limit the length of the notes + if len(personNotes) > 64000: + personNotes = None + optionsNickname = getNicknameFromActor(optionsActor) if not optionsNickname: if callingDomain.endswith('.onion') and \ @@ -7805,6 +7815,20 @@ class PubServer(BaseHTTPRequestHandler): callingDomain) self.server.POSTbusy = False return + if '&submitPersonNotes=' in optionsConfirmParams and personNotes: + if self.server.debug: + print('Change person notes') + handle = optionsNickname + '@' + optionsDomainFull + setPersonNotes(self.server.baseDir, + chooserNickname, + self.server.domain, + handle, personNotes) + self._redirect_headers(originPathStr + '/' + + self.server.defaultTimeline + + '?page='+str(pageNumber), cookie, + callingDomain) + self.server.POSTbusy = False + return if '&submitOnCalendar=' in optionsConfirmParams: onCalendar = None if 'onCalendar=' in optionsConfirmParams: diff --git a/person.py b/person.py index 09fe26570..c22487e37 100644 --- a/person.py +++ b/person.py @@ -1075,3 +1075,18 @@ def personUnsnooze(baseDir: str, nickname: str, domain: str, if writeSnoozedFile: writeSnoozedFile.write(content) writeSnoozedFile.close() + + +def setPersonNotes(baseDir: str, nickname: str, domain: str, + handle: str, notes: str) -> bool: + """Adds notes about a person + """ + if '@' not in handle: + return False + if handle.startswith('@'): + handle = handle[1:] + notesFilename = baseDir + '/accounts/' + \ + nickname + '@' + domain + '/notes/' + handle + '.txt' + with open(notesFilename, 'w+') as notesFile: + notesFile.write(notes) + return True diff --git a/translations/ar.json b/translations/ar.json index b54e8e450..b6762b3d2 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -254,5 +254,6 @@ "Grayscale": "درجات الرمادي", "Liked by": "نال إعجاب", "Solidaric": "تضامن", - "YouTube Replacement Domain": "استبدال نطاق يوتيوب" + "YouTube Replacement Domain": "استبدال نطاق يوتيوب", + "Notes": "ملاحظات" } diff --git a/translations/ca.json b/translations/ca.json index a9b786b2a..c4850e8ef 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -254,5 +254,6 @@ "Grayscale": "Escala de grisos", "Liked by": "M'agrada", "Solidaric": "Solidaritat", - "YouTube Replacement Domain": "Domini de substitució de YouTube" + "YouTube Replacement Domain": "Domini de substitució de YouTube", + "Notes": "Notes" } diff --git a/translations/cy.json b/translations/cy.json index 0edecbda1..afddb9c9f 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -254,5 +254,6 @@ "Grayscale": "Graddlwyd", "Liked by": "Hoffi", "Solidaric": "Undod", - "YouTube Replacement Domain": "Parth Amnewid YouTube" + "YouTube Replacement Domain": "Parth Amnewid YouTube", + "Notes": "Nodiadau" } diff --git a/translations/de.json b/translations/de.json index bc526301f..232e65d8b 100644 --- a/translations/de.json +++ b/translations/de.json @@ -254,5 +254,6 @@ "Grayscale": "Graustufen", "Liked by": "Gefallen von", "Solidaric": "Solidarität", - "YouTube Replacement Domain": "YouTube-Ersatzdomain" + "YouTube Replacement Domain": "YouTube-Ersatzdomain", + "Notes": "Anmerkungen" } diff --git a/translations/en.json b/translations/en.json index a472dffa7..1261381b6 100644 --- a/translations/en.json +++ b/translations/en.json @@ -254,5 +254,6 @@ "Grayscale": "Grayscale", "Liked by": "Liked by", "Solidaric": "Solidaric", - "YouTube Replacement Domain": "YouTube Replacement Domain" + "YouTube Replacement Domain": "YouTube Replacement Domain", + "Notes": "Notes" } diff --git a/translations/es.json b/translations/es.json index 04a95dfb4..333480767 100644 --- a/translations/es.json +++ b/translations/es.json @@ -254,5 +254,6 @@ "Grayscale": "Escala de grises", "Liked by": "Apreciado por", "Solidaric": "Solidaridad", - "YouTube Replacement Domain": "Dominio de reemplazo de YouTube" + "YouTube Replacement Domain": "Dominio de reemplazo de YouTube", + "Notes": "Notas" } diff --git a/translations/fr.json b/translations/fr.json index 3f9a8d363..f4b9dd0f4 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -254,5 +254,6 @@ "Grayscale": "Niveaux de gris", "Liked by": "Aimé par", "Solidaric": "Solidarité", - "YouTube Replacement Domain": "Domaine de remplacement YouTube" + "YouTube Replacement Domain": "Domaine de remplacement YouTube", + "Notes": "Remarques" } diff --git a/translations/ga.json b/translations/ga.json index cf69648d3..a812ff8e3 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -254,5 +254,6 @@ "Grayscale": "Liathscála", "Liked by": "Thaitin", "Solidaric": "Dlúthpháirtíocht", - "YouTube Replacement Domain": "Fearann Athsholáthair YouTube" + "YouTube Replacement Domain": "Fearann Athsholáthair YouTube", + "Notes": "Nótaí" } diff --git a/translations/hi.json b/translations/hi.json index b90225ecb..1b961804e 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -254,5 +254,6 @@ "Grayscale": "ग्रेस्केल", "Liked by": "द्वारा पसंद किया गया", "Solidaric": "एकजुटता", - "YouTube Replacement Domain": "YouTube रिप्लेसमेंट डोमेन" + "YouTube Replacement Domain": "YouTube रिप्लेसमेंट डोमेन", + "Notes": "टिप्पणियाँ" } diff --git a/translations/it.json b/translations/it.json index 2cecaa66d..b7c9c0507 100644 --- a/translations/it.json +++ b/translations/it.json @@ -254,5 +254,6 @@ "Grayscale": "Scala di grigi", "Liked by": "Mi è piaciuto", "Solidaric": "Solidarietà", - "YouTube Replacement Domain": "Dominio sostitutivo di YouTube" + "YouTube Replacement Domain": "Dominio sostitutivo di YouTube", + "Notes": "Appunti" } diff --git a/translations/ja.json b/translations/ja.json index 02a24543a..562118dd5 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -254,5 +254,6 @@ "Grayscale": "グレースケール", "Liked by": "好き", "Solidaric": "連帯", - "YouTube Replacement Domain": "YouTube交換ドメイン" + "YouTube Replacement Domain": "YouTube交換ドメイン", + "Notes": "ノート" } diff --git a/translations/oc.json b/translations/oc.json index 6203b272a..42ca4e070 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -250,5 +250,6 @@ "Grayscale": "Grayscale", "Liked by": "Liked by", "Solidaric": "Solidaric", - "YouTube Replacement Domain": "YouTube Replacement Domain" + "YouTube Replacement Domain": "YouTube Replacement Domain", + "Notes": "Notes" } diff --git a/translations/pt.json b/translations/pt.json index 19c0f4149..279e6c9ce 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -254,5 +254,6 @@ "Grayscale": "Escala de cinza", "Liked by": "Curtida por", "Solidaric": "Solidariedade", - "YouTube Replacement Domain": "Domínio de substituição do YouTube" + "YouTube Replacement Domain": "Domínio de substituição do YouTube", + "Notes": "Notas" } diff --git a/translations/ru.json b/translations/ru.json index bc322bf02..a5471654a 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -254,5 +254,6 @@ "Grayscale": "Оттенки серого", "Liked by": "Понравилось", "Solidaric": "солидарность", - "YouTube Replacement Domain": "Запасной домен YouTube" + "YouTube Replacement Domain": "Запасной домен YouTube", + "Notes": "Ноты" } diff --git a/translations/zh.json b/translations/zh.json index 16f29b92a..99f982152 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -253,5 +253,6 @@ "Grayscale": "灰阶", "Liked by": "喜欢的人", "Solidaric": "团结互助", - "YouTube Replacement Domain": "YouTube替换域" + "YouTube Replacement Domain": "YouTube替换域", + "Notes": "笔记" } diff --git a/webinterface.py b/webinterface.py index 45df3d27c..0dafb5556 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5566,10 +5566,10 @@ def htmlPersonOptions(translate: {}, baseDir: str, optionsStr += ' \n' optionsStr += ' \n' + handle = getNicknameFromActor(optionsActor) + '@' + optionsDomain optionsStr += \ '

' + translate['Options for'] + \ - ' @' + getNicknameFromActor(optionsActor) + '@' + \ - optionsDomain + '

\n' + ' @' + handle + '

\n' if emailAddress: optionsStr += \ '

' + translate['Email'] + \ @@ -5658,6 +5658,24 @@ def htmlPersonOptions(translate: {}, baseDir: str, ' \n' + personNotes = '' + personNotesFilename = \ + baseDir + '/accounts/' + nickname + '@' + domain + \ + '/notes/' + handle + '.txt' + if os.path.isfile(personNotesFilename): + with open(personNotesFilename, 'r') as fp: + personNotes = fp.read() + + optionsStr += \ + '

' + translate['Notes'] + ': \n' + optionsStr += '
\n' + optionsStr += \ + ' \n' + optionsStr += ' \n' optionsStr += '\n' optionsStr += '\n' From 697de38d9fd006cc84bb2b66143adf3201f44194 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 5 Aug 2020 22:14:55 +0100 Subject: [PATCH 10/34] Allow notes to be cleared --- daemon.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/daemon.py b/daemon.py index c7f720af8..48319b85b 100644 --- a/daemon.py +++ b/daemon.py @@ -7815,10 +7815,12 @@ class PubServer(BaseHTTPRequestHandler): callingDomain) self.server.POSTbusy = False return - if '&submitPersonNotes=' in optionsConfirmParams and personNotes: + if '&submitPersonNotes=' in optionsConfirmParams: if self.server.debug: print('Change person notes') handle = optionsNickname + '@' + optionsDomainFull + if not personNotes: + personNotes = '' setPersonNotes(self.server.baseDir, chooserNickname, self.server.domain, From 686272c40d6f809483391cea4a63c4c99ef73719 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 5 Aug 2020 22:24:35 +0100 Subject: [PATCH 11/34] Create notes directory --- person.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/person.py b/person.py index c22487e37..08ec19f59 100644 --- a/person.py +++ b/person.py @@ -1085,8 +1085,11 @@ def setPersonNotes(baseDir: str, nickname: str, domain: str, return False if handle.startswith('@'): handle = handle[1:] - notesFilename = baseDir + '/accounts/' + \ - nickname + '@' + domain + '/notes/' + handle + '.txt' + notesDir = baseDir + '/accounts/' + \ + nickname + '@' + domain + '/notes' + if not os.path.isdir(notesDir): + os.mkdir(notesDir) + notesFilename = notesDir + '/' + handle + '.txt' with open(notesFilename, 'w+') as notesFile: notesFile.write(notes) return True From e97b2281f1129d57f4f6607d48e2fc092c740aa8 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 5 Aug 2020 22:29:26 +0100 Subject: [PATCH 12/34] Redirect path is relative --- daemon.py | 6 +++--- person.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/daemon.py b/daemon.py index 48319b85b..89b0ad273 100644 --- a/daemon.py +++ b/daemon.py @@ -7809,7 +7809,7 @@ class PubServer(BaseHTTPRequestHandler): chooserNickname, self.server.domain, handle, petname) - self._redirect_headers(originPathStr + '/' + + self._redirect_headers(usersPath + '/' + self.server.defaultTimeline + '?page='+str(pageNumber), cookie, callingDomain) @@ -7825,7 +7825,7 @@ class PubServer(BaseHTTPRequestHandler): chooserNickname, self.server.domain, handle, personNotes) - self._redirect_headers(originPathStr + '/' + + self._redirect_headers(usersPath + '/' + self.server.defaultTimeline + '?page='+str(pageNumber), cookie, callingDomain) @@ -7849,7 +7849,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.domain, optionsNickname, optionsDomainFull) - self._redirect_headers(originPathStr + '/' + + self._redirect_headers(usersPath + '/' + self.server.defaultTimeline + '?page='+str(pageNumber), cookie, callingDomain) diff --git a/person.py b/person.py index 08ec19f59..2519a426e 100644 --- a/person.py +++ b/person.py @@ -1089,7 +1089,7 @@ def setPersonNotes(baseDir: str, nickname: str, domain: str, nickname + '@' + domain + '/notes' if not os.path.isdir(notesDir): os.mkdir(notesDir) - notesFilename = notesDir + '/' + handle + '.txt' + notesFilename = notesDir + '/' + handle + '.txt' with open(notesFilename, 'w+') as notesFile: notesFile.write(notes) return True From 070d8fe88e2660536b8a76f07f252b34234e9dd3 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 5 Aug 2020 22:32:10 +0100 Subject: [PATCH 13/34] Width of notes box --- epicyon-options.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/epicyon-options.css b/epicyon-options.css index 921fe849a..7c3dee7db 100644 --- a/epicyon-options.css +++ b/epicyon-options.css @@ -112,6 +112,11 @@ a:link { width: 15%; } +textarea { + font-size: var(--font-size4); + width: 90%; +} + @media screen and (min-width: 400px) { .followText { font-size: var(--follow-text-size1); From 5e1cb808214d3321c799ef779f9ee71d03fc3382 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 5 Aug 2020 22:34:14 +0100 Subject: [PATCH 14/34] Parse notes --- daemon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon.py b/daemon.py index 89b0ad273..86dc70ac7 100644 --- a/daemon.py +++ b/daemon.py @@ -7763,6 +7763,7 @@ class PubServer(BaseHTTPRequestHandler): personNotes = optionsConfirmParams.split('optionnotes=')[1] if '&' in personNotes: personNotes = personNotes.split('&')[0] + personNotes = urllib.parse.unquote(personNotes.strip()) # Limit the length of the notes if len(personNotes) > 64000: personNotes = None From 224bef348292034b42cfe8c55a3ca0e9de9ee22c Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 5 Aug 2020 22:43:28 +0100 Subject: [PATCH 15/34] Unquote plus sign --- daemon.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/daemon.py b/daemon.py index 86dc70ac7..9ea5a5ab5 100644 --- a/daemon.py +++ b/daemon.py @@ -1658,7 +1658,7 @@ class PubServer(BaseHTTPRequestHandler): # remove a shared item if htmlGET and '?rmshare=' in self.path: shareName = self.path.split('?rmshare=')[1] - shareName = urllib.parse.unquote(shareName.strip()) + shareName = urllib.parse.unquote_plus(shareName.strip()) usersPath = self.path.split('?rmshare=')[0] actor = \ self.server.httpPrefix + '://' + \ @@ -3357,7 +3357,7 @@ class PubServer(BaseHTTPRequestHandler): shareDescription = \ inReplyToUrl.replace('sharedesc:', '') shareDescription = \ - urllib.parse.unquote(shareDescription.strip()) + urllib.parse.unquote_plus(shareDescription.strip()) self.path = self.path.split('?replydm=')[0]+'/newdm' if self.server.debug: print('DEBUG: replydm path ' + self.path) @@ -6735,7 +6735,7 @@ class PubServer(BaseHTTPRequestHandler): moderationStr.split('=')[1].strip() moderationText = moderationText.replace('+', ' ') moderationText = \ - urllib.parse.unquote(moderationText.strip()) + urllib.parse.unquote_plus(moderationText.strip()) elif moderationStr.startswith('submitInfo'): msg = htmlModerationInfo(self.server.translate, self.server.baseDir, @@ -6909,7 +6909,7 @@ class PubServer(BaseHTTPRequestHandler): questionParams = questionParams.replace('+', ' ') questionParams = questionParams.replace('%3F', '') questionParams = \ - urllib.parse.unquote(questionParams.strip()) + urllib.parse.unquote_plus(questionParams.strip()) # post being voted on messageId = None if 'messageId=' in questionParams: @@ -6991,9 +6991,8 @@ class PubServer(BaseHTTPRequestHandler): searchStr = searchParams.split('searchtext=')[1] if '&' in searchStr: searchStr = searchStr.split('&')[0] - searchStr = searchStr.replace('+', ' ') searchStr = \ - urllib.parse.unquote(searchStr.strip()) + urllib.parse.unquote_plus(searchStr.strip()) searchStr2 = searchStr.lower().strip('\n').strip('\r') print('searchStr: ' + searchStr) if searchForEmoji: @@ -7199,7 +7198,7 @@ class PubServer(BaseHTTPRequestHandler): removeShareConfirmParams = \ removeShareConfirmParams.replace('+', ' ').strip() removeShareConfirmParams = \ - urllib.parse.unquote(removeShareConfirmParams) + urllib.parse.unquote_plus(removeShareConfirmParams) shareActor = removeShareConfirmParams.split('actor=')[1] if '&' in shareActor: shareActor = shareActor.split('&')[0] @@ -7262,7 +7261,7 @@ class PubServer(BaseHTTPRequestHandler): return if '&submitYes=' in removePostConfirmParams: removePostConfirmParams = \ - urllib.parse.unquote(removePostConfirmParams) + urllib.parse.unquote_plus(removePostConfirmParams) removeMessageId = \ removePostConfirmParams.split('messageId=')[1] if '&' in removeMessageId: @@ -7354,7 +7353,7 @@ class PubServer(BaseHTTPRequestHandler): return if '&submitView=' in followConfirmParams: followingActor = \ - urllib.parse.unquote(followConfirmParams) + urllib.parse.unquote_plus(followConfirmParams) followingActor = followingActor.split('actor=')[1] if '&' in followingActor: followingActor = followingActor.split('&')[0] @@ -7363,7 +7362,7 @@ class PubServer(BaseHTTPRequestHandler): return if '&submitYes=' in followConfirmParams: followingActor = \ - urllib.parse.unquote(followConfirmParams) + urllib.parse.unquote_plus(followConfirmParams) followingActor = followingActor.split('actor=')[1] if '&' in followingActor: followingActor = followingActor.split('&')[0] @@ -7436,7 +7435,7 @@ class PubServer(BaseHTTPRequestHandler): return if '&submitYes=' in followConfirmParams: followingActor = \ - urllib.parse.unquote(followConfirmParams) + urllib.parse.unquote_plus(followConfirmParams) followingActor = followingActor.split('actor=')[1] if '&' in followingActor: followingActor = followingActor.split('&')[0] @@ -7530,7 +7529,7 @@ class PubServer(BaseHTTPRequestHandler): return if '&submitYes=' in blockConfirmParams: blockingActor = \ - urllib.parse.unquote(blockConfirmParams) + urllib.parse.unquote_plus(blockConfirmParams) blockingActor = blockingActor.split('actor=')[1] if '&' in blockingActor: blockingActor = blockingActor.split('&')[0] @@ -7627,7 +7626,7 @@ class PubServer(BaseHTTPRequestHandler): return if '&submitYes=' in blockConfirmParams: blockingActor = \ - urllib.parse.unquote(blockConfirmParams) + urllib.parse.unquote_plus(blockConfirmParams) blockingActor = blockingActor.split('actor=')[1] if '&' in blockingActor: blockingActor = blockingActor.split('&')[0] @@ -7725,7 +7724,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy = False return optionsConfirmParams = \ - urllib.parse.unquote(optionsConfirmParams) + urllib.parse.unquote_plus(optionsConfirmParams) # page number to return to if 'pageNumber=' in optionsConfirmParams: pageNumberStr = optionsConfirmParams.split('pageNumber=')[1] @@ -7763,7 +7762,7 @@ class PubServer(BaseHTTPRequestHandler): personNotes = optionsConfirmParams.split('optionnotes=')[1] if '&' in personNotes: personNotes = personNotes.split('&')[0] - personNotes = urllib.parse.unquote(personNotes.strip()) + personNotes = urllib.parse.unquote_plus(personNotes.strip()) # Limit the length of the notes if len(personNotes) > 64000: personNotes = None From 7eadb4ea74ed4e2771bf79071be0a87fc2f83dfc Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 5 Aug 2020 22:47:32 +0100 Subject: [PATCH 16/34] Line length --- daemon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daemon.py b/daemon.py index 9ea5a5ab5..92fb2b4d6 100644 --- a/daemon.py +++ b/daemon.py @@ -6733,9 +6733,9 @@ class PubServer(BaseHTTPRequestHandler): if '=' in moderationStr: moderationText = \ moderationStr.split('=')[1].strip() - moderationText = moderationText.replace('+', ' ') + modText = moderationText.replace('+', ' ') moderationText = \ - urllib.parse.unquote_plus(moderationText.strip()) + urllib.parse.unquote_plus(modText.strip()) elif moderationStr.startswith('submitInfo'): msg = htmlModerationInfo(self.server.translate, self.server.baseDir, From e28327ad9702afb05b10888dde42d6e6087883c7 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 5 Aug 2020 22:49:03 +0100 Subject: [PATCH 17/34] Increase height of notes --- webinterface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webinterface.py b/webinterface.py index 0dafb5556..4ebf561b1 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5673,7 +5673,7 @@ def htmlPersonOptions(translate: {}, baseDir: str, translate['Submit'] + '
\n' optionsStr += \ ' \n' optionsStr += ' \n' From 34798bfd1509ef727ed6b807b3f56729fad0c87f Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 6 Aug 2020 17:21:46 +0100 Subject: [PATCH 18/34] Check that attributedTo is a string --- blog.py | 6 ++++-- git.py | 2 ++ inbox.py | 44 ++++++++++++++++++++++++-------------------- webinterface.py | 14 +++++++++----- 4 files changed, 39 insertions(+), 27 deletions(-) diff --git a/blog.py b/blog.py index 2101c8e3b..e41aacdff 100644 --- a/blog.py +++ b/blog.py @@ -170,8 +170,10 @@ def htmlBlogPostContent(authorized: bool, # get the handle of the author if postJsonObject['object'].get('attributedTo'): - actor = postJsonObject['object']['attributedTo'] - authorNickname = getNicknameFromActor(actor) + authorNickname = None + if isinstance(postJsonObject['object']['attributedTo'], str): + actor = postJsonObject['object']['attributedTo'] + authorNickname = getNicknameFromActor(actor) if authorNickname: authorDomain, authorPort = getDomainFromActor(actor) if authorDomain: diff --git a/git.py b/git.py index ffe8e131f..68680e33e 100644 --- a/git.py +++ b/git.py @@ -126,6 +126,8 @@ def convertPostToPatch(baseDir: str, nickname: str, domain: str, return False if not postJsonObject['object'].get('attributedTo'): return False + if not isinstance(postJsonObject['object']['attributedTo'], str): + return False if not isGitPatch(baseDir, nickname, domain, postJsonObject['object']['type'], postJsonObject['object']['summary'], diff --git a/inbox.py b/inbox.py index 2a9dfae12..15bf74a25 100644 --- a/inbox.py +++ b/inbox.py @@ -1422,12 +1422,15 @@ def receiveAnnounce(recentPostsCache: {}, # so that their avatar can be shown lookupActor = None if postJsonObject.get('attributedTo'): - lookupActor = postJsonObject['attributedTo'] + if isinstance(postJsonObject['attributedTo'], str): + lookupActor = postJsonObject['attributedTo'] else: if postJsonObject.get('object'): if isinstance(postJsonObject['object'], dict): if postJsonObject['object'].get('attributedTo'): - lookupActor = postJsonObject['object']['attributedTo'] + attrib = postJsonObject['object']['attributedTo'] + if isinstance(attrib, str): + lookupActor = attrib if lookupActor: if '/users/' in lookupActor or \ '/channel/' in lookupActor or \ @@ -2190,24 +2193,25 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int, postJsonObject['object'].get('summary') and \ postJsonObject['object'].get('attributedTo'): attributedTo = postJsonObject['object']['attributedTo'] - fromNickname = getNicknameFromActor(attributedTo) - fromDomain, fromPort = getDomainFromActor(attributedTo) - if fromPort: - if fromPort != 80 and fromPort != 443: - fromDomain += ':' + str(fromPort) - if receiveGitPatch(baseDir, nickname, domain, - postJsonObject['object']['type'], - postJsonObject['object']['summary'], - postJsonObject['object']['content'], - fromNickname, fromDomain): - gitPatchNotify(baseDir, handle, - postJsonObject['object']['summary'], - postJsonObject['object']['content'], - fromNickname, fromDomain) - elif '[PATCH]' in postJsonObject['object']['content']: - print('WARN: git patch not accepted - ' + - postJsonObject['object']['summary']) - return False + if isinstance(attributedTo, str): + fromNickname = getNicknameFromActor(attributedTo) + fromDomain, fromPort = getDomainFromActor(attributedTo) + if fromPort: + if fromPort != 80 and fromPort != 443: + fromDomain += ':' + str(fromPort) + if receiveGitPatch(baseDir, nickname, domain, + postJsonObject['object']['type'], + postJsonObject['object']['summary'], + postJsonObject['object']['content'], + fromNickname, fromDomain): + gitPatchNotify(baseDir, handle, + postJsonObject['object']['summary'], + postJsonObject['object']['content'], + fromNickname, fromDomain) + elif '[PATCH]' in postJsonObject['object']['content']: + print('WARN: git patch not accepted - ' + + postJsonObject['object']['summary']) + return False # replace YouTube links, so they get less tracking data replaceYouTube(postJsonObject, YTReplacementDomain) diff --git a/webinterface.py b/webinterface.py index 4ebf561b1..fbb1d8260 100644 --- a/webinterface.py +++ b/webinterface.py @@ -3813,8 +3813,9 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int, if showIcons: replyToLink = postJsonObject['object']['id'] if postJsonObject['object'].get('attributedTo'): - replyToLink += \ - '?mention=' + postJsonObject['object']['attributedTo'] + if isinstance(postJsonObject['object']['attributedTo'], str): + replyToLink += \ + '?mention=' + postJsonObject['object']['attributedTo'] if postJsonObject['object'].get('content'): mentionedActors = \ getMentionsFromHtml(postJsonObject['object']['content']) @@ -3985,7 +3986,9 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int, if showRepeatIcon: if isAnnounced: if postJsonObject['object'].get('attributedTo'): - attributedTo = postJsonObject['object']['attributedTo'] + attributedTo = '' + if isinstance(postJsonObject['object']['attributedTo'], str): + attributedTo = postJsonObject['object']['attributedTo'] if attributedTo.startswith(postActor): titleStr += \ ' \n' else: - announceNickname = \ - getNicknameFromActor(attributedTo) + announceNickname = None + if attributedTo: + announceNickname = getNicknameFromActor(attributedTo) if announceNickname: announceDomain, announcePort = \ getDomainFromActor(attributedTo) From a6e3731fa8d2ddc682a393d0216abf00ec10ec2d Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 6 Aug 2020 17:33:19 +0100 Subject: [PATCH 19/34] Descrption of encryption API --- devices.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/devices.py b/devices.py index 81e74a4fe..b2a4b6412 100644 --- a/devices.py +++ b/devices.py @@ -6,6 +6,29 @@ __maintainer__ = "Bob Mottram" __email__ = "bob@freedombone.net" __status__ = "Production" +# REST API overview +# +# To support Olm, the following APIs are required: +# +# * Uploading keys for a device (current app) +# POST /api/v1/crypto/keys/upload +# +# * Querying available devices of people you want to establish a session with +# POST /api/v1/crypto/keys/query +# +# * Claiming a pre-key (one-time-key) for each device you want to establish +# a session with +# POST /api/v1/crypto/keys/claim +# +# * Sending encrypted messages directly to specific devices of other people +# POST /api/v1/crypto/delivery +# +# * Collect encrypted messages addressed to the current device +# GET /api/v1/crypto/encrypted_messages +# +# * Clear all encrypted messages addressed to the current device +# POST /api/v1/crypto/encrypted_messages/clear + import os from utils import loadJson from utils import saveJson From a1b09a23bfa11351a303591da1709a7af6cb81b9 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 6 Aug 2020 17:49:13 +0100 Subject: [PATCH 20/34] Crypto API placeholder --- daemon.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/daemon.py b/daemon.py index 92fb2b4d6..3477029ea 100644 --- a/daemon.py +++ b/daemon.py @@ -5775,6 +5775,23 @@ class PubServer(BaseHTTPRequestHandler): postBytes, boundary) return pageNumber + def _cryptoAPI(self, path: str, authorized: bool) -> None: + # TODO + if path.startswith('/api/v1/crypto/keys/upload'): + self._200() + elif path.startswith('/api/v1/crypto/keys/query'): + self._200() + elif path.startswith('/api/v1/crypto/keys/claim'): + self._200() + elif path.startswith('/api/v1/crypto/delivery'): + self._200() + elif path.startswith('/api/v1/crypto/encrypted_messages/clear'): + self._200() + elif path.startswith('/api/v1/crypto/encrypted_messages'): + self._200() + else: + self._400() + def do_POST(self): POSTstartTime = time.time() POSTtimings = [] @@ -5848,6 +5865,11 @@ class PubServer(BaseHTTPRequestHandler): print('POST Not authorized') print(str(self.headers)) + if self.path.startswith('/api/v1/crypto/'): + self._cryptoAPI(self.path, authorized) + self.server.POSTbusy = False + return + # if this is a POST to the outbox then check authentication self.outboxAuthenticated = False self.postToNickname = None From 63712d6dca38b6539afdfbe69099237706db093c Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 6 Aug 2020 19:56:14 +0100 Subject: [PATCH 21/34] Some crypto endpoints need authorization --- daemon.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/daemon.py b/daemon.py index 3477029ea..b477a8330 100644 --- a/daemon.py +++ b/daemon.py @@ -5777,15 +5777,16 @@ class PubServer(BaseHTTPRequestHandler): def _cryptoAPI(self, path: str, authorized: bool) -> None: # TODO - if path.startswith('/api/v1/crypto/keys/upload'): + if authorized and path.startswith('/api/v1/crypto/keys/upload'): self._200() elif path.startswith('/api/v1/crypto/keys/query'): self._200() elif path.startswith('/api/v1/crypto/keys/claim'): self._200() - elif path.startswith('/api/v1/crypto/delivery'): + elif authorized and path.startswith('/api/v1/crypto/delivery'): self._200() - elif path.startswith('/api/v1/crypto/encrypted_messages/clear'): + elif (authorized and + path.startswith('/api/v1/crypto/encrypted_messages/clear')): self._200() elif path.startswith('/api/v1/crypto/encrypted_messages'): self._200() From e6ded5c7950507f54845a061f9a94b09e89604e0 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 6 Aug 2020 21:16:42 +0100 Subject: [PATCH 22/34] Change function names --- daemon.py | 11 ++++++----- devices.py | 22 +++++++++++----------- webinterface.py | 4 ++-- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/daemon.py b/daemon.py index b477a8330..cf74cf9a1 100644 --- a/daemon.py +++ b/daemon.py @@ -193,7 +193,7 @@ from bookmarks import undoBookmark from petnames import setPetName from followingCalendar import addPersonToCalendar from followingCalendar import removePersonFromCalendar -from devices import devicesCollection +from devices import E2EEdevicesCollection import os @@ -1545,10 +1545,11 @@ class PubServer(BaseHTTPRequestHandler): nickname = self.path.split('/users/') if '/' in nickname: nickname = nickname.split('/')[0] - devJson = devicesCollection(self.server.baseDir, - nickname, self.server.domain, - self.server.domainFull, - self.server.httpPrefix) + devJson = E2EEdevicesCollection(self.server.baseDir, + nickname, + self.server.domain, + self.server.domainFull, + self.server.httpPrefix) msg = json.dumps(devJson, ensure_ascii=False).encode('utf-8') self._set_headers('application/json', diff --git a/devices.py b/devices.py index b2a4b6412..b1d39f947 100644 --- a/devices.py +++ b/devices.py @@ -34,8 +34,8 @@ from utils import loadJson from utils import saveJson -def removeDevice(baseDir: str, nickname: str, domain: str, - deviceId: str) -> bool: +def E2EEremoveDevice(baseDir: str, nickname: str, domain: str, + deviceId: str) -> bool: """Unregisters a device for e2ee """ personDir = baseDir + '/accounts/' + nickname + '@' + domain @@ -46,12 +46,12 @@ def removeDevice(baseDir: str, nickname: str, domain: str, return False -def addDevice(baseDir: str, nickname: str, domain: str, - deviceId: str, name: str, claimUrl: str, - fingerprintPublicKey: str, - identityPublicKey: str, - fingerprintKeyType="Ed25519Key", - identityKeyType="Curve25519Key") -> bool: +def E2EEaddDevice(baseDir: str, nickname: str, domain: str, + deviceId: str, name: str, claimUrl: str, + fingerprintPublicKey: str, + identityPublicKey: str, + fingerprintKeyType="Ed25519Key", + identityKeyType="Curve25519Key") -> bool: """Registers a device for e2ee claimUrl could be something like: http://localhost:3000/users/admin/claim?id=11119 @@ -83,8 +83,8 @@ def addDevice(baseDir: str, nickname: str, domain: str, return saveJson(deviceDict, deviceFilename) -def devicesCollection(baseDir: str, nickname: str, domain: str, - domainFull: str, httpPrefix: str) -> {}: +def E2EEdevicesCollection(baseDir: str, nickname: str, domain: str, + domainFull: str, httpPrefix: str) -> {}: """Returns a list of registered devices """ personDir = baseDir + '/accounts/' + nickname + '@' + domain @@ -112,7 +112,7 @@ def devicesCollection(baseDir: str, nickname: str, domain: str, return devicesDict -def decryptMessageFromDevice(messageJson: {}) -> str: +def E2EEdecryptMessageFromDevice(messageJson: {}) -> str: """Locally decrypts a message on the device. This should probably be a link to a local script or native app, such that what the user sees isn't diff --git a/webinterface.py b/webinterface.py index fbb1d8260..e401e0db6 100644 --- a/webinterface.py +++ b/webinterface.py @@ -78,7 +78,7 @@ from git import isGitPatch from theme import getThemesList from petnames import getPetName from followingCalendar import receivingCalendarEvents -from devices import decryptMessageFromDevice +from devices import E2EEdecryptMessageFromDevice def getAltPath(actor: str, domainFull: str, callingDomain: str) -> str: @@ -4255,7 +4255,7 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int, if postJsonObject['object'].get('cipherText'): postJsonObject['object']['content'] = \ - decryptMessageFromDevice(postJsonObject['object']) + E2EEdecryptMessageFromDevice(postJsonObject['object']) if not postJsonObject['object'].get('content'): return '' From 4b3e6dc65c46827dd5deb3a95554d3094b511213 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 6 Aug 2020 21:56:14 +0100 Subject: [PATCH 23/34] Validate uploaded key --- daemon.py | 38 ++++++++++++++++++++++++++++++++++++++ devices.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/daemon.py b/daemon.py index cf74cf9a1..38d701b0b 100644 --- a/daemon.py +++ b/daemon.py @@ -194,6 +194,7 @@ from petnames import setPetName from followingCalendar import addPersonToCalendar from followingCalendar import removePersonFromCalendar from devices import E2EEdevicesCollection +from devices import E2EEvalidDevice import os @@ -5776,9 +5777,46 @@ class PubServer(BaseHTTPRequestHandler): postBytes, boundary) return pageNumber + def _cryptoAPIreadJson(self) -> {}: + messageBytes = None + maxCryptoMessageLength = 10240 + length = int(self.headers['Content-length']) + if length >= maxCryptoMessageLength: + print('WARN: post to crypto API is too long ' + + str(length) + ' bytes') + return {} + try: + messageBytes = self.rfile.read(length) + except SocketError as e: + if e.errno == errno.ECONNRESET: + print('WARN: POST messageBytes ' + + 'connection reset by peer') + else: + print('WARN: POST messageBytes socket error') + return {} + except ValueError as e: + print('ERROR: POST messageBytes rfile.read failed') + print(e) + return {} + + lenMessage = len(messageBytes) + if lenMessage > 10240: + print('WARN: post to crypto API is too long ' + + str(lenMessage) + ' bytes') + return {} + + return json.loads(messageBytes) + def _cryptoAPI(self, path: str, authorized: bool) -> None: # TODO if authorized and path.startswith('/api/v1/crypto/keys/upload'): + deviceKeys = self._cryptoAPIreadJson() + if not deviceKeys: + self._400() + return + if not E2EEvalidDevice(deviceKeys): + self._400() + return self._200() elif path.startswith('/api/v1/crypto/keys/query'): self._200() diff --git a/devices.py b/devices.py index b1d39f947..b81bf7f4a 100644 --- a/devices.py +++ b/devices.py @@ -46,6 +46,52 @@ def E2EEremoveDevice(baseDir: str, nickname: str, domain: str, return False +def E2EEvalidDevice(deviceJson: {}) -> bool: + """Returns true if the given json contains valid device keys + """ + if not isinstance(deviceJson, dict): + return False + if not deviceJson.get('deviceId'): + return False + if not isinstance(deviceJson['deviceId'], str): + return False + if not deviceJson.get('type'): + return False + if not isinstance(deviceJson['type'], str): + return False + if deviceJson['type'] != 'Device': + return False + if not deviceJson.get('claim'): + return False + if not isinstance(deviceJson['claim'], str): + return False + if not deviceJson.get('fingerprintKey'): + return False + if not isinstance(deviceJson['fingerprintKey'], dict): + return False + if not deviceJson['fingerprintKey'].get('type'): + return False + if not isinstance(deviceJson['fingerprintKey']['type'], str): + return False + if not deviceJson['fingerprintKey'].get('publicKeyBase64'): + return False + if not isinstance(deviceJson['fingerprintKey']['publicKeyBase64'], str): + return False + if not deviceJson.get('identityKey'): + return False + if not isinstance(deviceJson['identityKey'], dict): + return False + if not deviceJson['identityKey'].get('type'): + return False + if not isinstance(deviceJson['identityKey']['type'], str): + return False + if not deviceJson['identityKey'].get('publicKeyBase64'): + return False + if not isinstance(deviceJson['identityKey']['publicKeyBase64'], str): + return False + return True + + def E2EEaddDevice(baseDir: str, nickname: str, domain: str, deviceId: str, name: str, claimUrl: str, fingerprintPublicKey: str, From 4a34ee0e80108cf2eac8a2aa24532017926fc58b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 6 Aug 2020 22:23:17 +0100 Subject: [PATCH 24/34] Store uploaded device key --- daemon.py | 25 ++++++++++++++++++++++++- devices.py | 4 ++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/daemon.py b/daemon.py index 38d701b0b..f4a2beb20 100644 --- a/daemon.py +++ b/daemon.py @@ -195,6 +195,7 @@ from followingCalendar import addPersonToCalendar from followingCalendar import removePersonFromCalendar from devices import E2EEdevicesCollection from devices import E2EEvalidDevice +from devices import E2EEaddDevice import os @@ -1051,6 +1052,8 @@ class PubServer(BaseHTTPRequestHandler): return 1 def _isAuthorized(self) -> bool: + self.authorizedNickname = None + if self.path.startswith('/icons/') or \ self.path.startswith('/avatars/') or \ self.path.startswith('/favicon.ico'): @@ -1064,6 +1067,7 @@ class PubServer(BaseHTTPRequestHandler): tokenStr = tokenStr.split(';')[0].strip() if self.server.tokensLookup.get(tokenStr): nickname = self.server.tokensLookup[tokenStr] + self.authorizedNickname = nickname # default to the inbox of the person if self.path == '/': self.path = '/users/' + nickname + '/inbox' @@ -5778,6 +5782,8 @@ class PubServer(BaseHTTPRequestHandler): return pageNumber def _cryptoAPIreadJson(self) -> {}: + """Obtains json from POST to the crypto API + """ messageBytes = None maxCryptoMessageLength = 10240 length = int(self.headers['Content-length']) @@ -5808,8 +5814,10 @@ class PubServer(BaseHTTPRequestHandler): return json.loads(messageBytes) def _cryptoAPI(self, path: str, authorized: bool) -> None: - # TODO if authorized and path.startswith('/api/v1/crypto/keys/upload'): + if not self.authorizedNickname: + self._400() + return deviceKeys = self._cryptoAPIreadJson() if not deviceKeys: self._400() @@ -5817,17 +5825,32 @@ class PubServer(BaseHTTPRequestHandler): if not E2EEvalidDevice(deviceKeys): self._400() return + E2EEaddDevice(self.server.baseDir, + self.authorizedNickname, + self.server.domain, + deviceKeys['deviceId'], + deviceKeys['name'], + deviceKeys['claim'], + deviceKeys['fingerprintKey']['publicKeyBase64'], + deviceKeys['identityKey']['publicKeyBase64'], + deviceKeys['fingerprintKey']['type'], + deviceKeys['identityKey']['type']) self._200() elif path.startswith('/api/v1/crypto/keys/query'): + # TODO self._200() elif path.startswith('/api/v1/crypto/keys/claim'): + # TODO self._200() elif authorized and path.startswith('/api/v1/crypto/delivery'): + # TODO self._200() elif (authorized and path.startswith('/api/v1/crypto/encrypted_messages/clear')): + # TODO self._200() elif path.startswith('/api/v1/crypto/encrypted_messages'): + # TODO self._200() else: self._400() diff --git a/devices.py b/devices.py index b81bf7f4a..69c8d2ca7 100644 --- a/devices.py +++ b/devices.py @@ -59,6 +59,10 @@ def E2EEvalidDevice(deviceJson: {}) -> bool: return False if not isinstance(deviceJson['type'], str): return False + if not deviceJson.get('name'): + return False + if not isinstance(deviceJson['name'], str): + return False if deviceJson['type'] != 'Device': return False if not deviceJson.get('claim'): From fac9296caf28cec2fe0ee6bedc8c411e379543e1 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 6 Aug 2020 22:24:47 +0100 Subject: [PATCH 25/34] Comment --- daemon.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/daemon.py b/daemon.py index f4a2beb20..b40fafb51 100644 --- a/daemon.py +++ b/daemon.py @@ -5814,6 +5814,8 @@ class PubServer(BaseHTTPRequestHandler): return json.loads(messageBytes) def _cryptoAPI(self, path: str, authorized: bool) -> None: + """POST or GET with the crypto API + """ if authorized and path.startswith('/api/v1/crypto/keys/upload'): if not self.authorizedNickname: self._400() From cc909109081ec6ccbea6c0114ddfadc60ee74be8 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 7 Aug 2020 21:40:53 +0100 Subject: [PATCH 26/34] Don't show long hashtags --- blocking.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/blocking.py b/blocking.py index 836f85ca2..070b4c83e 100644 --- a/blocking.py +++ b/blocking.py @@ -118,6 +118,9 @@ def removeBlock(baseDir: str, nickname: str, domain: str, def isBlockedHashtag(baseDir: str, hashtag: str) -> bool: """Is the given hashtag blocked? """ + # avoid very long hashtags + if len(hashtag) > 32: + return True globalBlockingFilename = baseDir + '/accounts/blocking.txt' if os.path.isfile(globalBlockingFilename): hashtag = hashtag.strip('\n').strip('\r') From 2f2034d0eca22fef369372a0799cce6443f83ff6 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 7 Aug 2020 21:43:54 +0100 Subject: [PATCH 27/34] Long hashtags are invalid --- content.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/content.py b/content.py index 2d2bc7d88..488601702 100644 --- a/content.py +++ b/content.py @@ -267,6 +267,9 @@ def addWebLinks(content: str) -> str: def validHashTag(hashtag: str) -> bool: """Returns true if the give hashtag contains valid characters """ + # long hashtags are not valid + if len(hashtag) >= 32: + return False validChars = set('0123456789' + 'abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') From 0b70ee70d88be3b398acb5e368fa6f44f91e9844 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 7 Aug 2020 21:51:34 +0100 Subject: [PATCH 28/34] Test for long hashtag --- tests.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests.py b/tests.py index 2f85cd218..70da4a6aa 100644 --- a/tests.py +++ b/tests.py @@ -1684,6 +1684,13 @@ def testWebLinks(): '

filepopout=' + \ 'TemplateAttachmentRichPopout' + exampleText = \ + '

Test1 test2 #YetAnotherExcessivelyLongwindedAndBoringHashtag<\p>' + resultText = removeLongWords(addWebLinks(exampleText), 40, []) + assert(resultText == + '

Test1 test2 ' + '#YetAnotherExcessivelyLongwindedAndBorin\ngHashtag

') + def testAddEmoji(): print('testAddEmoji') From 6e648018e8cdb8d554641c5d03f46b3ad78472d4 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 9 Aug 2020 20:19:40 +0000 Subject: [PATCH 29/34] More invidious sites --- webinterface.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/webinterface.py b/webinterface.py index e401e0db6..6f8030950 100644 --- a/webinterface.py +++ b/webinterface.py @@ -3060,7 +3060,10 @@ def addEmbeddedVideoFromSites(translate: {}, content: str, return content invidiousSites = ('https://invidio.us', - 'axqzx4s6s54s32yentfqojs3x5i7faxza6xo3ehd4' + + 'https://invidious.snopyta.org', + 'http://c7hqkpkpemu6e7emz5b4vy' + + 'z7idjgdvgaaa3dyimmeojqbgpea3xqjoid.onion', + 'http://axqzx4s6s54s32yentfqojs3x5i7faxza6xo3ehd4' + 'bzzsg2ii4fv2iid.onion') for videoSite in invidiousSites: if '"' + videoSite in content: From b4071629e03e080f00744a93ba42034937b6878d Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 11 Aug 2020 13:02:10 +0100 Subject: [PATCH 30/34] Notes background color on options screen --- epicyon-options.css | 1 + 1 file changed, 1 insertion(+) diff --git a/epicyon-options.css b/epicyon-options.css index 7c3dee7db..3fe023a1f 100644 --- a/epicyon-options.css +++ b/epicyon-options.css @@ -115,6 +115,7 @@ a:link { textarea { font-size: var(--font-size4); width: 90%; + background-color: var(--text-entry-background); } @media screen and (min-width: 400px) { From ea80dbb49601eb6a66627434f092a8ba0c99ae7b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 11 Aug 2020 12:57:34 +0000 Subject: [PATCH 31/34] Upload multiple keys --- daemon.py | 49 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/daemon.py b/daemon.py index b40fafb51..fdc7c2edc 100644 --- a/daemon.py +++ b/daemon.py @@ -5824,20 +5824,43 @@ class PubServer(BaseHTTPRequestHandler): if not deviceKeys: self._400() return - if not E2EEvalidDevice(deviceKeys): - self._400() + if isinstance(deviceKeys, list): + keyCtr = 0 + for devKey in deviceKeys: + if not E2EEvalidDevice(devKey): + continue + E2EEaddDevice(self.server.baseDir, + self.authorizedNickname, + self.server.domain, + devKey['deviceId'], + devKey['name'], + devKey['claim'], + devKey['fingerprintKey']['publicKeyBase64'], + devKey['identityKey']['publicKeyBase64'], + devKey['fingerprintKey']['type'], + devKey['identityKey']['type']) + keyCtr += 1 + if keyCtr > 10: + break + self._200() return - E2EEaddDevice(self.server.baseDir, - self.authorizedNickname, - self.server.domain, - deviceKeys['deviceId'], - deviceKeys['name'], - deviceKeys['claim'], - deviceKeys['fingerprintKey']['publicKeyBase64'], - deviceKeys['identityKey']['publicKeyBase64'], - deviceKeys['fingerprintKey']['type'], - deviceKeys['identityKey']['type']) - self._200() + elif isinstance(deviceKeys, dict): + if not E2EEvalidDevice(deviceKeys): + self._400() + return + E2EEaddDevice(self.server.baseDir, + self.authorizedNickname, + self.server.domain, + deviceKeys['deviceId'], + deviceKeys['name'], + deviceKeys['claim'], + deviceKeys['fingerprintKey']['publicKeyBase64'], + deviceKeys['identityKey']['publicKeyBase64'], + deviceKeys['fingerprintKey']['type'], + deviceKeys['identityKey']['type']) + self._200() + return + self._400() elif path.startswith('/api/v1/crypto/keys/query'): # TODO self._200() From b7be635aa32b3ababef341b274ac8da4c8c10fc0 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 11 Aug 2020 17:18:22 +0000 Subject: [PATCH 32/34] Return devices for a handle --- daemon.py | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- tests.py | 2 +- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/daemon.py b/daemon.py index fdc7c2edc..736d57f55 100644 --- a/daemon.py +++ b/daemon.py @@ -5781,6 +5781,49 @@ class PubServer(BaseHTTPRequestHandler): postBytes, boundary) return pageNumber + def _cryptoAPIreadHandle(self): + """Reads handle + """ + messageBytes = None + maxDeviceIdLength = 2048 + length = int(self.headers['Content-length']) + if length >= maxDeviceIdLength: + print('WARN: handle post to crypto API is too long ' + + str(length) + ' bytes') + return {} + try: + messageBytes = self.rfile.read(length) + except SocketError as e: + if e.errno == errno.ECONNRESET: + print('WARN: handle POST messageBytes ' + + 'connection reset by peer') + else: + print('WARN: handle POST messageBytes socket error') + return {} + except ValueError as e: + print('ERROR: handle POST messageBytes rfile.read failed') + print(e) + return {} + + lenMessage = len(messageBytes) + if lenMessage > 2048: + print('WARN: handle post to crypto API is too long ' + + str(lenMessage) + ' bytes') + return {} + + handle = messageBytes.decode("utf-8") + if not handle: + return None + if '@' not in handle: + return None + if '[' in handle: + return json.loads(messageBytes) + if handle.startswith('@'): + handle = handle[1:] + if '@' not in handle: + return None + return handle.strip() + def _cryptoAPIreadJson(self) -> {}: """Obtains json from POST to the crypto API """ @@ -5813,6 +5856,34 @@ class PubServer(BaseHTTPRequestHandler): return json.loads(messageBytes) + def _cryptoAPIQuery(self, callingDomain: str) -> bool: + handle = self._cryptoAPIreadHandle() + if not handle: + return False + if isinstance(handle, str): + personDir = self.server.baseDir + '/accounts/' + handle + if not os.path.isdir(personDir + '/devices'): + return False + devicesList = [] + for subdir, dirs, files in os.walk(personDir + '/devices'): + for f in files: + deviceFilename = os.path.join(personDir + '/devices', f) + if not os.path.isfile(deviceFilename): + continue + contentJson = loadJson(deviceFilename) + if contentJson: + devicesList.append(contentJson) + # return the list of devices for this handle + msg = \ + json.dumps(devicesList, + ensure_ascii=False).encode('utf-8') + self._set_headers('application/json', + len(msg), + None, callingDomain) + self._write(msg) + return True + return False + def _cryptoAPI(self, path: str, authorized: bool) -> None: """POST or GET with the crypto API """ @@ -5862,8 +5933,8 @@ class PubServer(BaseHTTPRequestHandler): return self._400() elif path.startswith('/api/v1/crypto/keys/query'): - # TODO - self._200() + if not self._cryptoAPIQuery(): + self._400() elif path.startswith('/api/v1/crypto/keys/claim'): # TODO self._200() diff --git a/tests.py b/tests.py index 70da4a6aa..947d9d98c 100644 --- a/tests.py +++ b/tests.py @@ -1685,7 +1685,7 @@ def testWebLinks(): 'TemplateAttachmentRichPopout' exampleText = \ - '

Test1 test2 #YetAnotherExcessivelyLongwindedAndBoringHashtag<\p>' + '

Test1 test2 #YetAnotherExcessivelyLongwindedAndBoringHashtag

' resultText = removeLongWords(addWebLinks(exampleText), 40, []) assert(resultText == '

Test1 test2 ' From fedbe18ad27c011b3540a4829c3d3ae9b9b420fa Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 11 Aug 2020 18:21:56 +0100 Subject: [PATCH 33/34] Register one device at a time --- daemon.py | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/daemon.py b/daemon.py index 736d57f55..ad9b6756e 100644 --- a/daemon.py +++ b/daemon.py @@ -5888,6 +5888,7 @@ class PubServer(BaseHTTPRequestHandler): """POST or GET with the crypto API """ if authorized and path.startswith('/api/v1/crypto/keys/upload'): + # register a device to an authorized account if not self.authorizedNickname: self._400() return @@ -5895,27 +5896,7 @@ class PubServer(BaseHTTPRequestHandler): if not deviceKeys: self._400() return - if isinstance(deviceKeys, list): - keyCtr = 0 - for devKey in deviceKeys: - if not E2EEvalidDevice(devKey): - continue - E2EEaddDevice(self.server.baseDir, - self.authorizedNickname, - self.server.domain, - devKey['deviceId'], - devKey['name'], - devKey['claim'], - devKey['fingerprintKey']['publicKeyBase64'], - devKey['identityKey']['publicKeyBase64'], - devKey['fingerprintKey']['type'], - devKey['identityKey']['type']) - keyCtr += 1 - if keyCtr > 10: - break - self._200() - return - elif isinstance(deviceKeys, dict): + if isinstance(deviceKeys, dict): if not E2EEvalidDevice(deviceKeys): self._400() return From 995adf0ceb8f3fcc0def8a9d01062c2e4a55e433 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 11 Aug 2020 18:24:03 +0100 Subject: [PATCH 34/34] Comments --- daemon.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/daemon.py b/daemon.py index ad9b6756e..05d0bf911 100644 --- a/daemon.py +++ b/daemon.py @@ -5914,6 +5914,8 @@ class PubServer(BaseHTTPRequestHandler): return self._400() elif path.startswith('/api/v1/crypto/keys/query'): + # given a handle (nickname@domain) return the devices + # registered to that handle if not self._cryptoAPIQuery(): self._400() elif path.startswith('/api/v1/crypto/keys/claim'):