diff --git a/Makefile b/Makefile index 5420916ad..034be8abb 100644 --- a/Makefile +++ b/Makefile @@ -23,4 +23,4 @@ clean: rm -f deploy/*~ rm -f translations/*~ rm -rf __pycache__ - rm calendar.css blog.css epicyon.css follow.css login.css options.css search.css suspended.css + rm -f calendar.css blog.css epicyon.css follow.css login.css options.css search.css suspended.css diff --git a/cache.py b/cache.py index 0505ef543..19cda4084 100644 --- a/cache.py +++ b/cache.py @@ -8,11 +8,45 @@ __status__ = "Production" import os import datetime +from session import urlExists from utils import loadJson from utils import saveJson from utils import getFileCaseInsensitive +def _removePersonFromCache(baseDir: str, personUrl: str, + personCache: {}) -> bool: + """Removes an actor from the cache + """ + cacheFilename = baseDir + '/cache/actors/' + \ + personUrl.replace('/', '#')+'.json' + if os.path.isfile(cacheFilename): + try: + os.remove(cacheFilename) + except BaseException: + pass + if personCache.get(personUrl): + del personCache[personUrl] + + +def checkForChangedActor(session, baseDir: str, + httpPrefix: str, domainFull: str, + personUrl: str, avatarUrl: str, personCache: {}, + timeoutSec: int): + """Checks if the avatar url exists and if not then + the actor has probably changed without receiving an actor/Person Update. + So clear the actor from the cache and it will be refreshed when the next + post from them is sent + """ + if not session or not avatarUrl: + return + if domainFull in avatarUrl: + return + if urlExists(session, avatarUrl, timeoutSec, httpPrefix, domainFull): + return + _removePersonFromCache(baseDir, personUrl, personCache) + + def storePersonInCache(baseDir: str, personUrl: str, personJson: {}, personCache: {}, allowWriteToFile: bool) -> None: diff --git a/daemon.py b/daemon.py index d4e9dd30c..577afe37f 100644 --- a/daemon.py +++ b/daemon.py @@ -228,6 +228,7 @@ from content import extractMediaInFormPOST from content import saveMediaInFormPOST from content import extractTextFieldsInPOST from media import removeMetaData +from cache import checkForChangedActor from cache import storePersonInCache from cache import getPersonFromCache from httpsig import verifyPostHeaders @@ -5488,6 +5489,15 @@ class PubServer(BaseHTTPRequestHandler): PGPfingerprint = getPGPfingerprint(actorJson) if actorJson.get('alsoKnownAs'): alsoKnownAs = actorJson['alsoKnownAs'] + + if self.server.session: + checkForChangedActor(self.server.session, + self.server.baseDir, + self.server.httpPrefix, + self.server.domainFull, + optionsActor, optionsProfileUrl, + self.server.personCache, 5) + msg = htmlPersonOptions(self.server.defaultTimeline, self.server.cssCache, self.server.translate, @@ -10051,6 +10061,16 @@ class PubServer(BaseHTTPRequestHandler): # replace https://domain/@nick with https://domain/users/nick if self.path.startswith('/@'): self.path = self.path.replace('/@', '/users/') + # replace https://domain/@nick/statusnumber + # with https://domain/users/nick/statuses/statusnumber + nickname = self.path.split('/users/')[1] + if '/' in nickname: + statusNumberStr = nickname.split('/')[1] + if statusNumberStr.isdigit(): + nickname = nickname.split('/')[0] + self.path = \ + self.path.replace('/users/' + nickname + '/', + '/users/' + nickname + '/statuses/') # turn off dropdowns on new post screen noDropDown = False diff --git a/emoji/copyleft.png b/emoji/1F12F.png similarity index 100% rename from emoji/copyleft.png rename to emoji/1F12F.png diff --git a/inbox.py b/inbox.py index 26d9e5268..8f216e1c6 100644 --- a/inbox.py +++ b/inbox.py @@ -2726,6 +2726,7 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, print('DEBUG: checking http header signature') pprint(queueJson['httpHeaders']) postStr = json.dumps(queueJson['post']) + httpSignatureFailed = False if not verifyPostHeaders(httpPrefix, pubKey, queueJson['httpHeaders'], @@ -2733,19 +2734,17 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, queueJson['digest'], postStr, debug): + httpSignatureFailed = True print('Queue: Header signature check failed') - pprint(queueJson['httpHeaders']) - if os.path.isfile(queueFilename): - os.remove(queueFilename) - if len(queue) > 0: - queue.pop(0) - continue - - if debug: - print('DEBUG: http header signature check success') + if debug: + pprint(queueJson['httpHeaders']) + else: + if debug: + print('DEBUG: http header signature check success') # check if a json signature exists on this post - checkJsonSignature = False + hasJsonSignature = False + jwebsigType = None originalJson = queueJson['original'] if originalJson.get('@context') and \ originalJson.get('signature'): @@ -2754,41 +2753,59 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, jwebsig = originalJson['signature'] # signature exists and is of the expected type if jwebsig.get('type') and jwebsig.get('signatureValue'): - if jwebsig['type'] == 'RsaSignature2017': + jwebsigType = jwebsig['type'] + if jwebsigType == 'RsaSignature2017': if hasValidContext(originalJson): - checkJsonSignature = True + hasJsonSignature = True else: print('unrecognised @context: ' + str(originalJson['@context'])) # strict enforcement of json signatures - if verifyAllSignatures and \ - not checkJsonSignature: - print('inbox post does not have a jsonld signature ' + - keyId + ' ' + str(originalJson)) - if os.path.isfile(queueFilename): - os.remove(queueFilename) - if len(queue) > 0: - queue.pop(0) - continue - - if checkJsonSignature and verifyAllSignatures: - # use the original json message received, not one which may have - # been modified along the way - if not verifyJsonSignature(originalJson, pubKey): - if debug: - print('WARN: jsonld inbox signature check failed ' + - keyId + ' ' + pubKey + ' ' + str(originalJson)) + if not hasJsonSignature: + if httpSignatureFailed: + if jwebsigType: + print('Queue: Header signature check failed and does ' + + 'not have a recognised jsonld signature type ' + + jwebsigType) else: - print('WARN: jsonld inbox signature check failed ' + - keyId) + print('Queue: Header signature check failed and ' + + 'does not have jsonld signature') + if debug: + pprint(queueJson['httpHeaders']) + + if verifyAllSignatures: + print('Queue: inbox post does not have a jsonld signature ' + + keyId + ' ' + str(originalJson)) + + if httpSignatureFailed or verifyAllSignatures: if os.path.isfile(queueFilename): os.remove(queueFilename) if len(queue) > 0: queue.pop(0) continue - else: - print('jsonld inbox signature check success ' + keyId) + else: + if httpSignatureFailed or verifyAllSignatures: + # use the original json message received, not one which + # may have been modified along the way + if not verifyJsonSignature(originalJson, pubKey): + if debug: + print('WARN: jsonld inbox signature check failed ' + + keyId + ' ' + pubKey + ' ' + str(originalJson)) + else: + print('WARN: jsonld inbox signature check failed ' + + keyId) + if os.path.isfile(queueFilename): + os.remove(queueFilename) + if len(queue) > 0: + queue.pop(0) + continue + else: + if httpSignatureFailed: + print('jsonld inbox signature check success ' + + 'via relay ' + keyId) + else: + print('jsonld inbox signature check success ' + keyId) # set the id to the same as the post filename # This makes the filename and the id consistent diff --git a/session.py b/session.py index cf235fd76..d9c37907f 100644 --- a/session.py +++ b/session.py @@ -53,6 +53,37 @@ def createSession(proxyType: str): return session +def urlExists(session, url: str, timeoutSec=3, + httpPrefix='https', domain='testdomain') -> bool: + if not isinstance(url, str): + print('url: ' + str(url)) + print('ERROR: urlExists failed, url should be a string') + return False + sessionParams = {} + sessionHeaders = {} + sessionHeaders['User-Agent'] = 'Epicyon/' + __version__ + if domain: + sessionHeaders['User-Agent'] += \ + '; +' + httpPrefix + '://' + domain + '/' + if not session: + print('WARN: urlExists failed, no session specified') + return True + try: + result = session.get(url, headers=sessionHeaders, + params=sessionParams, + timeout=timeoutSec) + if result: + if result.status_code == 200 or \ + result.status_code == 304: + return True + else: + print('urlExists for ' + url + ' returned ' + + str(result.status_code)) + except BaseException: + pass + return False + + def getJson(session, url: str, headers: {}, params: {}, version='1.2.0', httpPrefix='https', domain='testdomain') -> {}: @@ -72,6 +103,7 @@ def getJson(session, url: str, headers: {}, params: {}, '; +' + httpPrefix + '://' + domain + '/' if not session: print('WARN: getJson failed, no session specified for getJson') + return None try: result = session.get(url, headers=sessionHeaders, params=sessionParams) return result.json() diff --git a/theme/default/banner.png b/theme/default/banner.png index 46eb37e91..5860c6b9c 100644 Binary files a/theme/default/banner.png and b/theme/default/banner.png differ diff --git a/theme/default/search_banner.png b/theme/default/search_banner.png index 4f2811684..5860c6b9c 100644 Binary files a/theme/default/search_banner.png and b/theme/default/search_banner.png differ