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'):