From 76e8167ce7f8c0cdf6b0b36d5e58d096e9eb977f Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 9 Jul 2019 15:20:23 +0100 Subject: [PATCH] Updating of capabilities --- acceptreject.py | 18 +++---- announce.py | 4 +- capabilities.py | 84 +++++++++++++++++++++++++++++++ daemon.py | 8 ++- epicyon.py | 20 ++++---- follow.py | 10 ++-- inbox.py | 86 +++++++++++++++++++++++++++++--- like.py | 2 +- posts.py | 68 ++++++++++++++++++------- session.py | 4 +- tests.py | 128 +++++++++++++++++++++++++++++++++++++----------- utils.py | 2 +- 12 files changed, 346 insertions(+), 88 deletions(-) diff --git a/acceptreject.py b/acceptreject.py index 6d9a65c85..e6030707e 100644 --- a/acceptreject.py +++ b/acceptreject.py @@ -18,7 +18,7 @@ from utils import getNicknameFromActor from utils import domainPermitted from utils import followPerson -def createAcceptReject(baseDir: str,federationList: [],ocapGranted: {}, \ +def createAcceptReject(baseDir: str,federationList: [], \ nickname: str,domain: str,port: int, \ toUrl: str,ccUrl: str,httpPrefix: str, \ objectJson: {},ocapJson,acceptType: str) -> {}: @@ -31,7 +31,7 @@ def createAcceptReject(baseDir: str,federationList: [],ocapGranted: {}, \ if not objectJson.get('actor'): return None - if not urlPermitted(objectJson['actor'],federationList,ocapGranted,"inbox:write"): + if not urlPermitted(objectJson['actor'],federationList,"inbox:write"): return None if port!=80 and port!=443: @@ -52,28 +52,28 @@ def createAcceptReject(baseDir: str,federationList: [],ocapGranted: {}, \ newAccept['capabilities']=ocapJson return newAccept -def createAccept(baseDir: str,federationList: [],ocapGranted: {}, \ +def createAccept(baseDir: str,federationList: [], \ nickname: str,domain: str,port: int, \ toUrl: str,ccUrl: str,httpPrefix: str, \ objectJson: {}) -> {}: # create capabilities accept ocapNew=capabilitiesAccept(baseDir,httpPrefix,nickname,domain,port,toUrl,True) - return createAcceptReject(baseDir,federationList,ocapGranted, \ + return createAcceptReject(baseDir,federationList, \ nickname,domain,port, \ toUrl,ccUrl,httpPrefix, \ objectJson,ocapNew,'Accept') -def createReject(baseDir: str,federationList: [],ocapGranted: {}, \ +def createReject(baseDir: str,federationList: [], \ nickname: str,domain: str,port: int, \ toUrl: str,ccUrl: str,httpPrefix: str, \ objectJson: {}) -> {}: - return createAcceptReject(baseDir,federationList,ocapGranted, \ + return createAcceptReject(baseDir,federationList, \ nickname,domain,port, \ toUrl,ccUrl, \ httpPrefix,objectJson,None,'Reject') def acceptFollow(baseDir: str,domain : str,messageJson: {}, \ - federationList: [],ocapGranted: {},debug : bool) -> None: + federationList: [],debug : bool) -> None: if not messageJson.get('object'): return if not messageJson['object'].get('type'): @@ -161,7 +161,7 @@ def receiveAcceptReject(session,baseDir: str, \ httpPrefix: str,domain :str,port: int, \ sendThreads: [],postLog: [],cachedWebfingers: {}, \ personCache: {},messageJson: {},federationList: [], \ - ocapGranted: {},debug : bool) -> bool: + debug : bool) -> bool: """Receives an Accept or Reject within the POST section of HTTPServer """ if messageJson['type']!='Accept' and messageJson['type']!='Reject': @@ -185,7 +185,7 @@ def receiveAcceptReject(session,baseDir: str, \ print('DEBUG: '+messageJson['type']+' does not contain a nickname') return False handle=nickname.lower()+'@'+domain.lower() - acceptFollow(baseDir,domain,messageJson,federationList,ocapGranted,debug) + acceptFollow(baseDir,domain,messageJson,federationList,debug) if debug: print('DEBUG: Uh, '+messageJson['type']+', I guess') return True diff --git a/announce.py b/announce.py index 6ee63ecee..6e11506aa 100644 --- a/announce.py +++ b/announce.py @@ -12,7 +12,7 @@ from utils import getStatusNumber from utils import createOutboxDir from utils import urlPermitted -def createAnnounce(baseDir: str,federationList: [], ocapGranted: {}, \ +def createAnnounce(baseDir: str,federationList: [], \ nickname: str, domain: str, port: int, \ toUrl: str, ccUrl: str, httpPrefix: str, \ objectUrl: str, saveToFile: bool) -> {}: @@ -22,7 +22,7 @@ def createAnnounce(baseDir: str,federationList: [], ocapGranted: {}, \ followers url objectUrl is typically the url of the message, corresponding to url or atomUri in createPostBase """ - if not urlPermitted(objectUrl,federationList,ocapGranted,"inbox:write"): + if not urlPermitted(objectUrl,federationList,"inbox:write"): return None if port!=80 and port!=443: diff --git a/capabilities.py b/capabilities.py index 597bcd01a..dfb350241 100644 --- a/capabilities.py +++ b/capabilities.py @@ -16,6 +16,9 @@ from utils import getNicknameFromActor from utils import getDomainFromActor def getOcapFilename(baseDir :str,nickname: str,domain: str,actor :str,subdir: str) -> str: + """Returns the filename for a particular capability accepted or granted + Also creates directories as needed + """ if ':' in domain: domain=domain.split(':')[0] @@ -151,3 +154,84 @@ def capabilitiesGrantedSave(baseDir :str,nickname :str,domain :str,ocap: {}) -> with open(ocapFilename, 'w') as fp: commentjson.dump(ocap, fp, indent=4, sort_keys=False) return True + +def capabilitiesUpdate(baseDir: str,httpPrefix: str, \ + nickname: str,domain: str, port: int, \ + updateActor: str, \ + updateCaps: []) -> {}: + """Used to sends an update for a change of object capabilities + Note that the capability id gets changed with a new random token + so that the old capabilities can't continue to be used + """ + + # reject excessively long actors + if len(updateActor)>256: + return None + + fullDomain=domain + if port!=80 and port !=443: + fullDomain=domain+':'+str(port) + + # Get the filename of the capability + ocapFilename=getOcapFilename(baseDir,nickname,fullDomain,updateActor,'accept') + + # The capability should already exist for it to be updated + if not os.path.isfile(ocapFilename): + return None + + # create an update activity + ocapUpdate = { + 'type': 'Update', + 'actor': httpPrefix+'://'+fullDomain+'/users/'+nickname, + 'to': [updateActor], + 'cc': [], + 'object': {} + } + + # read the existing capability + with open(ocapFilename, 'r') as fp: + ocapJson=commentjson.load(fp) + + # set the new capabilities list. eg. ["inbox:write","objects:read"] + ocapJson['capability']=updateCaps + + # change the id, so that the old capabilities can't continue to be used + updateActorNickname=getNicknameFromActor(updateActor) + updateActorDomain,updateActorPort=getDomainFromActor(updateActor) + if updateActorPort: + ocapId=updateActorNickname+'@'+updateActorDomain+':'+str(updateActorPort)+'#'+createPassword(32) + else: + ocapId=updateActorNickname+'@'+updateActorDomain+'#'+createPassword(32) + ocapJson['id']=httpPrefix+"://"+fullDomain+"/caps/"+ocapId + ocapUpdate['object']=ocapJson + + # save it again + with open(ocapFilename, 'w') as fp: + commentjson.dump(ocapJson, fp, indent=4, sort_keys=False) + + return ocapUpdate + +def capabilitiesReceiveUpdate(baseDir :str, \ + nickname :str,domain :str,port :int, \ + actor :str, \ + newCapabilitiesId :str, \ + capabilityList :[], debug :bool) -> bool: + """An update for a capability or the given actor has arrived + """ + ocapFilename= \ + getOcapFilename(baseDir,nickname,domain,actor,'granted') + if not os.path.isfile(ocapFilename): + if debug: + print('DEBUG: capabilities file not found during update') + print(ocapFilename) + return False + + with open(ocapFilename, 'r') as fp: + ocapJson=commentjson.load(fp) + ocapJson['id']=newCapabilitiesId + ocapJson['capability']=capabilityList + + with open(ocapFilename, 'w') as fp: + commentjson.dump(ocapJson, fp, indent=4, sort_keys=False) + return True + return False diff --git a/daemon.py b/daemon.py index ec0fe8d69..5c2cf9503 100644 --- a/daemon.py +++ b/daemon.py @@ -450,8 +450,7 @@ class PubServer(BaseHTTPRequestHandler): if not inboxPermittedMessage(self.server.domain, \ messageJson, \ - self.server.federationList, \ - self.server.ocapGranted): + self.server.federationList): if self.server.debug: # https://www.youtube.com/watch?v=K3PrSj9XEu4 print('DEBUG: Ah Ah Ah') @@ -498,7 +497,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy=False def runDaemon(baseDir: str,domain: str,port=80,httpPrefix='https', \ - fedList=[],ocapAlways=False,ocapGranted={}, \ + fedList=[],ocapAlways=False, \ useTor=False,debug=False) -> None: if len(domain)==0: domain='localhost' @@ -514,7 +513,6 @@ def runDaemon(baseDir: str,domain: str,port=80,httpPrefix='https', \ httpd.httpPrefix=httpPrefix httpd.debug=debug httpd.federationList=fedList.copy() - httpd.ocapGranted=ocapGranted.copy() httpd.baseDir=baseDir httpd.personCache={} httpd.cachedWebfingers={} @@ -538,6 +536,6 @@ def runDaemon(baseDir: str,domain: str,port=80,httpPrefix='https', \ httpd.personCache,httpd.inboxQueue, \ domain,port,useTor,httpd.federationList, \ httpd.ocapAlways, \ - httpd.ocapGranted,debug),daemon=True) + debug),daemon=True) httpd.thrInboxQueue.start() httpd.serve_forever() diff --git a/epicyon.py b/epicyon.py index 69482f8a6..541852446 100644 --- a/epicyon.py +++ b/epicyon.py @@ -320,8 +320,6 @@ else: if configFederationList: federationList=configFederationList -ocapGranted={} - if federationList: print('Federating with: '+str(federationList)) @@ -348,13 +346,13 @@ if args.testdata: deleteAllPosts(baseDir,nickname,domain,'outbox') followPerson(baseDir,nickname,domain,'admin',domain,federationList,True) followerOfPerson(baseDir,nickname,domain,'admin',domain,federationList,True) - createPublicPost(baseDir,nickname,domain,port,httpPrefix,"like, this is totally just a test, man",False,True,False,ocapGranted) - createPublicPost(baseDir,nickname,domain,port,httpPrefix,"Zoiks!!!",False,True,False,ocapGranted) - createPublicPost(baseDir,nickname,domain,port,httpPrefix,"Hey scoob we need like a hundred more milkshakes",False,True,False,ocapGranted) - createPublicPost(baseDir,nickname,domain,port,httpPrefix,"Getting kinda spooky around here",False,True,False,ocapGranted) - createPublicPost(baseDir,nickname,domain,port,httpPrefix,"And they would have gotten away with it too if it wasn't for those pesky hackers",False,True,False,ocapGranted) - createPublicPost(baseDir,nickname,domain,port,httpPrefix,"man, these centralized sites are, like, the worst!",False,True,False,ocapGranted) - createPublicPost(baseDir,nickname,domain,port,httpPrefix,"another mystery solved hey",False,True,False,ocapGranted) - createPublicPost(baseDir,nickname,domain,port,httpPrefix,"let's go bowling",False,True,False,ocapGranted) + createPublicPost(baseDir,nickname,domain,port,httpPrefix,"like, this is totally just a test, man",False,True,False) + createPublicPost(baseDir,nickname,domain,port,httpPrefix,"Zoiks!!!",False,True,False) + createPublicPost(baseDir,nickname,domain,port,httpPrefix,"Hey scoob we need like a hundred more milkshakes",False,True,False) + createPublicPost(baseDir,nickname,domain,port,httpPrefix,"Getting kinda spooky around here",False,True,False) + createPublicPost(baseDir,nickname,domain,port,httpPrefix,"And they would have gotten away with it too if it wasn't for those pesky hackers",False,True,False) + createPublicPost(baseDir,nickname,domain,port,httpPrefix,"man, these centralized sites are, like, the worst!",False,True,False) + createPublicPost(baseDir,nickname,domain,port,httpPrefix,"another mystery solved hey",False,True,False) + createPublicPost(baseDir,nickname,domain,port,httpPrefix,"let's go bowling",False,True,False) -runDaemon(baseDir,domain,port,httpPrefix,federationList,ocapAlways,ocapGranted,useTor,debug) +runDaemon(baseDir,domain,port,httpPrefix,federationList,ocapAlways,useTor,debug) diff --git a/follow.py b/follow.py index daeeecdde..c8257c6b5 100644 --- a/follow.py +++ b/follow.py @@ -221,7 +221,7 @@ def receiveFollowRequest(session,baseDir: str,httpPrefix: str, \ port: int,sendThreads: [],postLog: [], \ cachedWebfingers: {},personCache: {}, \ messageJson: {},federationList: [], \ - ocapGranted: {},debug : bool) -> bool: + debug : bool) -> bool: """Receives a follow request within the POST section of HTTPServer """ if not messageJson['type'].startswith('Follow'): @@ -281,7 +281,7 @@ def receiveFollowRequest(session,baseDir: str,httpPrefix: str, \ print('DEBUG: sending Accept for follow request which arrived at '+ \ nicknameToFollow+'@'+domainToFollow+' back to '+nickname+'@'+domain) personUrl=messageJson['actor'] - acceptJson=createAccept(baseDir,federationList,ocapGranted, \ + acceptJson=createAccept(baseDir,federationList, \ nicknameToFollow,domainToFollow,port, \ personUrl,'',httpPrefix,messageJson) if debug: @@ -295,7 +295,7 @@ def receiveFollowRequest(session,baseDir: str,httpPrefix: str, \ nicknameToFollow,domainToFollow,port, \ nickname,domain,fromPort, '', \ httpPrefix,True,clientToServer, \ - federationList, ocapGranted, \ + federationList, \ sendThreads,postLog,cachedWebfingers, \ personCache,debug) @@ -303,7 +303,7 @@ def sendFollowRequest(session,baseDir: str, \ nickname: str,domain: str,port: int,httpPrefix: str, \ followNickname: str,followDomain: str, \ followPort: bool,followHttpPrefix: str, \ - clientToServer: bool,federationList: [],ocapGranted: {}, \ + clientToServer: bool,federationList: [], \ sendThreads: [],postLog: [],cachedWebfingers: {}, \ personCache: {},debug : bool) -> {}: """Gets the json object for sending a follow request @@ -339,7 +339,7 @@ def sendFollowRequest(session,baseDir: str, \ followNickname,followDomain,followPort, \ 'https://www.w3.org/ns/activitystreams#Public', \ httpPrefix,True,clientToServer, \ - federationList, ocapGranted, \ + federationList, \ sendThreads,postLog,cachedWebfingers,personCache, debug) return newFollowJson diff --git a/inbox.py b/inbox.py index 3b15e49a2..19e503709 100644 --- a/inbox.py +++ b/inbox.py @@ -16,6 +16,9 @@ from shutil import copyfile from utils import urlPermitted from utils import createInboxQueueDir from utils import getStatusNumber +from utils import getDomainFromActor +from utils import getNicknameFromActor +from utils import domainPermitted from httpsig import verifyPostHeaders from session import createSession from session import getJson @@ -27,6 +30,7 @@ from cache import storePersonInCache from acceptreject import receiveAcceptReject from capabilities import getOcapFilename from capabilities import CapablePost +from capabilities import capabilitiesReceiveUpdate def getPersonPubKey(session,personUrl: str,personCache: {},debug: bool) -> str: if not personUrl: @@ -68,7 +72,7 @@ def inboxMessageHasParams(messageJson: {}) -> bool: return False return True -def inboxPermittedMessage(domain: str,messageJson: {},federationList: [],ocapGranted: {}) -> bool: +def inboxPermittedMessage(domain: str,messageJson: {},federationList: []) -> bool: """ check that we are receiving from a permitted domain """ testParam='actor' @@ -79,14 +83,14 @@ def inboxPermittedMessage(domain: str,messageJson: {},federationList: [],ocapGra if domain in actor: return True - if not urlPermitted(actor,federationList,ocapGranted,"inbox:write"): + if not urlPermitted(actor,federationList,"inbox:write"): return False if messageJson['type']!='Follow': if messageJson.get('object'): if messageJson['object'].get('inReplyTo'): inReplyTo=messageJson['object']['inReplyTo'] - if not urlPermitted(inReplyTo,federationList,ocapGranted): + if not urlPermitted(inReplyTo,federationList): return False return True @@ -299,7 +303,62 @@ def inboxPostRecipients(baseDir :str,postJsonObject :{},httpPrefix :str,domain : return recipientsDict -def runInboxQueue(baseDir: str,httpPrefix: str,sendThreads: [],postLog: [],cachedWebfingers: {},personCache: {},queue: [],domain: str,port: int,useTor: bool,federationList: [],ocapAlways: bool,ocapGranted: {},debug: bool) -> None: +def receiveUpdate(session,baseDir: str, \ + httpPrefix: str,domain :str,port: int, \ + sendThreads: [],postLog: [],cachedWebfingers: {}, \ + personCache: {},messageJson: {},federationList: [], \ + debug : bool) -> bool: + """Receives an Update activity within the POST section of HTTPServer + """ + if messageJson['type']!='Update': + return False + if not messageJson.get('actor'): + if debug: + print('DEBUG: '+messageJson['type']+' has no actor') + return False + if not messageJson.get('object'): + if debug: + print('DEBUG: '+messageJson['type']+' has no object') + return False + if not isinstance(messageJson['object'], dict): + if debug: + print('DEBUG: '+messageJson['type']+' object is not a dict') + return False + if not messageJson['object'].get('type'): + if debug: + print('DEBUG: '+messageJson['type']+' object has no type') + return False + if '/users/' not in messageJson['actor']: + if debug: + print('DEBUG: "users" missing from actor in '+messageJson['type']) + return False + domain,tempPort=getDomainFromActor(messageJson['actor']) + if not domainPermitted(domain,federationList): + if debug: + print('DEBUG: '+messageJson['type']+' from domain not permitted - '+domain) + return False + nickname=getNicknameFromActor(messageJson['actor']) + if not nickname: + if debug: + print('DEBUG: '+messageJson['type']+' does not contain a nickname') + return False + handle=nickname.lower()+'@'+domain.lower() + if messageJson['object'].get('capability') and messageJson['object'].get('scope'): + domain,tempPort=getDomainFromActor(messageJson['object']['scope']) + nickname=getNicknameFromActor(messageJson['object']['scope']) + + if messageJson['object']['type']=='Capability': + if capabilitiesReceiveUpdate(baseDir,nickname,domain,port, + messageJson['actor'], \ + messageJson['object']['id'], \ + messageJson['object']['capability'], \ + debug): + if debug: + print('DEBUG: An update was received') + return True + return False + +def runInboxQueue(baseDir: str,httpPrefix: str,sendThreads: [],postLog: [],cachedWebfingers: {},personCache: {},queue: [],domain: str,port: int,useTor: bool,federationList: [],ocapAlways: bool,debug: bool) -> None: """Processes received items and moves them to the appropriate directories """ @@ -389,7 +448,7 @@ def runInboxQueue(baseDir: str,httpPrefix: str,sendThreads: [],postLog: [],cache cachedWebfingers, personCache, queueJson['post'], \ - federationList,ocapGranted, \ + federationList, \ debug): if debug: print('DEBUG: Follow accepted from '+keyId) @@ -403,7 +462,7 @@ def runInboxQueue(baseDir: str,httpPrefix: str,sendThreads: [],postLog: [],cache cachedWebfingers, personCache, queueJson['post'], \ - federationList,ocapGranted, \ + federationList, \ debug): if debug: print('DEBUG: Accept/Reject received from '+keyId) @@ -411,6 +470,21 @@ def runInboxQueue(baseDir: str,httpPrefix: str,sendThreads: [],postLog: [],cache queue.pop(0) continue + if receiveUpdate(session, \ + baseDir,httpPrefix, \ + domain,port, \ + sendThreads,postLog, \ + cachedWebfingers, + personCache, + queueJson['post'], \ + federationList, \ + debug): + if debug: + print('DEBUG: Update accepted from '+keyId) + os.remove(queueFilename) + queue.pop(0) + continue + # get recipients list recipientsDict=inboxPostRecipients(baseDir,queueJson['post'],httpPrefix,domain,port) diff --git a/like.py b/like.py index 17552ded8..d6ddf38b9 100644 --- a/like.py +++ b/like.py @@ -17,7 +17,7 @@ def like(baseDir: str,federationList: [],nickname: str,domain: str,port: int, \ and ccUrl might be a specific person whose post was liked objectUrl is typically the url of the message, corresponding to url or atomUri in createPostBase """ - if not urlPermitted(objectUrl,federationList,ocapGranted,"inbox:write"): + if not urlPermitted(objectUrl,federationList,"inbox:write"): return None if port!=80 and port!=443: diff --git a/posts.py b/posts.py index 7ac9d0f03..05fe08223 100644 --- a/posts.py +++ b/posts.py @@ -30,7 +30,10 @@ from httpsig import createSignedHeader from utils import getStatusNumber from utils import createPersonDir from utils import urlPermitted +from utils import getNicknameFromActor +from utils import getDomainFromActor from capabilities import getOcapFilename +from capabilities import capabilitiesUpdate try: from BeautifulSoup import BeautifulSoup except ImportError: @@ -153,7 +156,7 @@ def getPersonBox(session,wfRequest: {},personCache: {}, \ def getPosts(session,outboxUrl: str,maxPosts: int,maxMentions: int, \ maxEmoji: int,maxAttachments: int, \ - federationList: [], ocapGranted: {},\ + federationList: [],\ personCache: {},raw: bool,simple: bool) -> {}: personPosts={} if not outboxUrl: @@ -193,7 +196,7 @@ def getPosts(session,outboxUrl: str,maxPosts: int,maxMentions: int, \ if tagItem['icon'].get('url'): # No emoji from non-permitted domains if urlPermitted(tagItem['icon']['url'], \ - federationList,ocapGranted, \ + federationList, \ "objects:read"): emojiName=tagItem['name'] emojiIcon=tagItem['icon']['url'] @@ -217,7 +220,7 @@ def getPosts(session,outboxUrl: str,maxPosts: int,maxMentions: int, \ if item['object']['inReplyTo']: # No replies to non-permitted domains if not urlPermitted(item['object']['inReplyTo'], \ - federationList,ocapGranted, \ + federationList, \ "objects:read"): continue inReplyTo = item['object']['inReplyTo'] @@ -227,7 +230,7 @@ def getPosts(session,outboxUrl: str,maxPosts: int,maxMentions: int, \ if item['object']['conversation']: # no conversations originated in non-permitted domains if urlPermitted(item['object']['conversation'], \ - federationList,ocapGranted,"objects:read"): + federationList,"objects:read"): conversation = item['object']['conversation'] attachment = [] @@ -237,7 +240,7 @@ def getPosts(session,outboxUrl: str,maxPosts: int,maxMentions: int, \ if attach.get('name') and attach.get('url'): # no attachments from non-permitted domains if urlPermitted(attach['url'], \ - federationList,ocapGranted, \ + federationList, \ "objects:read"): attachment.append([attach['name'],attach['url']]) @@ -319,7 +322,6 @@ def savePostToBox(baseDir: str,httpPrefix: str,postId: str, \ def createPostBase(baseDir: str,nickname: str, domain: str, port: int, \ toUrl: str, ccUrl: str, httpPrefix: str, content: str, \ followersOnly: bool, saveToFile: bool, clientToServer: bool, \ - ocapGranted: {}, \ inReplyTo=None, inReplyToAtomUri=None, subject=None) -> {}: """Creates a message """ @@ -487,7 +489,7 @@ def postIsAddressedToFollowers(baseDir: str, def createPublicPost(baseDir: str, nickname: str, domain: str, port: int,httpPrefix: str, \ content: str, followersOnly: bool, saveToFile: bool, - clientToServer: bool, ocapGranted: {},\ + clientToServer: bool,\ inReplyTo=None, inReplyToAtomUri=None, subject=None) -> {}: """Public post to the outbox """ @@ -495,10 +497,10 @@ def createPublicPost(baseDir: str, 'https://www.w3.org/ns/activitystreams#Public', \ httpPrefix+'://'+domain+'/users/'+nickname+'/followers', \ httpPrefix, content, followersOnly, saveToFile, \ - clientToServer, ocapGranted, \ + clientToServer, \ inReplyTo, inReplyToAtomUri, subject) -def threadSendPost(session,postJsonObject: {},federationList: [],ocapGranted: {},\ +def threadSendPost(session,postJsonObject: {},federationList: [],\ inboxUrl: str, baseDir: str,signatureHeaderJson: {},postLog: [], debug :bool) -> None: """Sends a post with exponential backoff @@ -507,7 +509,7 @@ def threadSendPost(session,postJsonObject: {},federationList: [],ocapGranted: {} backoffTime=60 for attempt in range(20): postResult = postJson(session,postJsonObject,federationList, \ - ocapGranted,inboxUrl,signatureHeaderJson, \ + inboxUrl,signatureHeaderJson, \ "inbox:write") if postResult: if debug: @@ -534,7 +536,7 @@ def sendPost(session,baseDir: str,nickname: str, domain: str, port: int, \ toNickname: str, toDomain: str, toPort: int, cc: str, \ httpPrefix: str, content: str, followersOnly: bool, \ saveToFile: bool, clientToServer: bool, \ - federationList: [], ocapGranted: {},\ + federationList: [],\ sendThreads: [], postLog: [], cachedWebfingers: {},personCache: {}, \ debug=False,inReplyTo=None,inReplyToAtomUri=None,subject=None) -> int: """Post to another inbox @@ -583,7 +585,6 @@ def sendPost(session,baseDir: str,nickname: str, domain: str, port: int, \ createPostBase(baseDir,nickname,domain,port, \ toPersonId,cc,httpPrefix,content, \ followersOnly,saveToFile,clientToServer, \ - ocapGranted, \ inReplyTo,inReplyToAtomUri,subject) # get the senders private key @@ -607,7 +608,6 @@ def sendPost(session,baseDir: str,nickname: str, domain: str, port: int, \ thr = threadWithTrace(target=threadSendPost,args=(session, \ postJsonObject.copy(), \ federationList, \ - ocapGranted, \ inboxUrl,baseDir, \ signatureHeaderJson.copy(), \ postLog, @@ -639,7 +639,7 @@ def sendSignedJson(postJsonObject: {},session,baseDir: str, \ nickname: str, domain: str, port: int, \ toNickname: str, toDomain: str, toPort: int, cc: str, \ httpPrefix: str, saveToFile: bool, clientToServer: bool, \ - federationList: [], ocapGranted: {}, \ + federationList: [], \ sendThreads: [], postLog: [], cachedWebfingers: {}, \ personCache: {}, debug: bool) -> int: """Sends a signed json object to an inbox/outbox @@ -715,7 +715,6 @@ def sendSignedJson(postJsonObject: {},session,baseDir: str, \ args=(session, \ postJsonObject.copy(), \ federationList, \ - ocapGranted, \ inboxUrl,baseDir, \ signatureHeaderJson.copy(), \ postLog, @@ -754,7 +753,7 @@ def sendToFollowers(session,baseDir: str, nickname,domain,port, \ toNickname,toDomain,toPort, \ cc,httpPrefix,True,clientToServer, \ - federationList,ocapGranted, \ + federationList, \ sendThreads,postLog,cachedWebfingers, \ personCache,debug) @@ -900,7 +899,6 @@ def getPublicPostsOfPerson(nickname: str,domain: str, \ personCache={} cachedWebfingers={} federationList=[] - ocapGranted={} httpPrefix='https' handle=httpPrefix+"://"+domain+"/@"+nickname @@ -917,6 +915,40 @@ def getPublicPostsOfPerson(nickname: str,domain: str, \ maxEmoji=10 maxAttachments=5 userPosts = getPosts(session,personUrl,30,maxMentions,maxEmoji, \ - maxAttachments,federationList,ocapGranted, \ + maxAttachments,federationList, \ personCache,raw,simple) #print(str(userPosts)) + +def sendCapabilitiesUpdate(session,baseDir: str,httpPrefix: str, \ + nickname: str,domain: str,port: int, \ + followerUrl,updateCaps: [], \ + sendThreads: [],postLog: [], \ + cachedWebfingers: {},personCache: {}, \ + federationList :[],debug :bool) -> int: + """When the capabilities for a follower are changed this + sends out an update. followerUrl is the actor of the follower. + """ + updateJson=capabilitiesUpdate(baseDir,httpPrefix, \ + nickname,domain,port, \ + followerUrl, \ + updateCaps) + + if not updateJson: + return 1 + + if debug: + pprint(updateJson) + print('DEBUG: sending capabilities update from '+ \ + nickname+'@'+domain+' port '+ str(port) + \ + ' to '+followerUrl) + + clientToServer=False + followerNickname=getNicknameFromActor(followerUrl) + followerDomain,followerPort=getDomainFromActor(followerUrl) + return sendSignedJson(updateJson,session,baseDir, \ + nickname,domain,port, \ + followerNickname,followerDomain,followerPort, '', \ + httpPrefix,True,clientToServer, \ + federationList, \ + sendThreads,postLog,cachedWebfingers, \ + personCache,debug) diff --git a/session.py b/session.py index a3603576b..6f8e78c4a 100644 --- a/session.py +++ b/session.py @@ -39,7 +39,7 @@ def getJson(session,url: str,headers: {},params: {}) -> {}: pass return None -def postJson(session,postJsonObject: {},federationList: [],ocapGranted: {},inboxUrl: str,headers: {},capability: str) -> str: +def postJson(session,postJsonObject: {},federationList: [],inboxUrl: str,headers: {},capability: str) -> str: """Post a json message to the inbox of another person Supplying a capability, such as "inbox:write" """ @@ -47,7 +47,7 @@ def postJson(session,postJsonObject: {},federationList: [],ocapGranted: {},inbox # always allow capability requests if not capability.startswith('cap'): # check that we are posting to a permitted domain - if not urlPermitted(inboxUrl,federationList,ocapGranted,capability): + if not urlPermitted(inboxUrl,federationList,capability): return None postResult = session.post(url = inboxUrl, data = json.dumps(postJsonObject), headers=headers) diff --git a/tests.py b/tests.py index 3f24aa08f..8f0dae335 100644 --- a/tests.py +++ b/tests.py @@ -10,6 +10,8 @@ import base64 import time import os, os.path import shutil +import commentjson +from pprint import pprint from person import createPerson from Crypto.Hash import SHA256 from httpsig import signPostHeaders @@ -25,6 +27,7 @@ from posts import sendPost from posts import archivePosts from posts import noOfFollowersOnDomain from posts import groupFollowersByDomain +from posts import sendCapabilitiesUpdate from follow import clearFollows from follow import clearFollowers from utils import followPerson @@ -111,7 +114,7 @@ def testThreads(): thr.join() assert thr.isAlive()==False -def createServerAlice(path: str,domain: str,port: int,federationList: [],ocapGranted: {},hasFollows: bool,hasPosts :bool,ocapAlways: bool): +def createServerAlice(path: str,domain: str,port: int,federationList: [],hasFollows: bool,hasPosts :bool,ocapAlways: bool): print('Creating test server: Alice on port '+str(port)) if os.path.isdir(path): shutil.rmtree(path) @@ -129,15 +132,15 @@ def createServerAlice(path: str,domain: str,port: int,federationList: [],ocapGra followPerson(path,nickname,domain,'bob','127.0.0.100:61936',federationList,True) followerOfPerson(path,nickname,domain,'bob','127.0.0.100:61936',federationList,True) if hasPosts: - createPublicPost(path,nickname, domain, port,httpPrefix, "No wise fish would go anywhere without a porpoise", False, True, clientToServer,ocapGranted) - createPublicPost(path,nickname, domain, port,httpPrefix, "Curiouser and curiouser!", False, True, clientToServer,ocapGranted) - createPublicPost(path,nickname, domain, port,httpPrefix, "In the gardens of memory, in the palace of dreams, that is where you and I shall meet", False, True, clientToServer,ocapGranted) + createPublicPost(path,nickname, domain, port,httpPrefix, "No wise fish would go anywhere without a porpoise", False, True, clientToServer) + createPublicPost(path,nickname, domain, port,httpPrefix, "Curiouser and curiouser!", False, True, clientToServer) + createPublicPost(path,nickname, domain, port,httpPrefix, "In the gardens of memory, in the palace of dreams, that is where you and I shall meet", False, True, clientToServer) global testServerAliceRunning testServerAliceRunning = True print('Server running: Alice') - runDaemon(path,domain,port,httpPrefix,federationList,ocapAlways,ocapGranted,useTor,True) + runDaemon(path,domain,port,httpPrefix,federationList,ocapAlways,useTor,True) -def createServerBob(path: str,domain: str,port: int,federationList: [],ocapGranted: {},hasFollows: bool,hasPosts :bool,ocapAlways :bool): +def createServerBob(path: str,domain: str,port: int,federationList: [],hasFollows: bool,hasPosts :bool,ocapAlways :bool): print('Creating test server: Bob on port '+str(port)) if os.path.isdir(path): shutil.rmtree(path) @@ -155,15 +158,15 @@ def createServerBob(path: str,domain: str,port: int,federationList: [],ocapGrant followPerson(path,nickname,domain,'alice','127.0.0.50:61935',federationList,True) followerOfPerson(path,nickname,domain,'alice','127.0.0.50:61935',federationList,True) if hasPosts: - createPublicPost(path,nickname, domain, port,httpPrefix, "It's your life, live it your way.", False, True, clientToServer,ocapGranted) - createPublicPost(path,nickname, domain, port,httpPrefix, "One of the things I've realised is that I am very simple", False, True, clientToServer,ocapGranted) - createPublicPost(path,nickname, domain, port,httpPrefix, "Quantum physics is a bit of a passion of mine", False, True, clientToServer,ocapGranted) + createPublicPost(path,nickname, domain, port,httpPrefix, "It's your life, live it your way.", False, True, clientToServer) + createPublicPost(path,nickname, domain, port,httpPrefix, "One of the things I've realised is that I am very simple", False, True, clientToServer) + createPublicPost(path,nickname, domain, port,httpPrefix, "Quantum physics is a bit of a passion of mine", False, True, clientToServer) global testServerBobRunning testServerBobRunning = True print('Server running: Bob') - runDaemon(path,domain,port,httpPrefix,federationList,ocapAlways,ocapGranted,useTor,True) + runDaemon(path,domain,port,httpPrefix,federationList,ocapAlways,useTor,True) -def createServerEve(path: str,domain: str,port: int,federationList: [],ocapGranted: {},hasFollows: bool,hasPosts :bool,ocapAlways :bool): +def createServerEve(path: str,domain: str,port: int,federationList: [],hasFollows: bool,hasPosts :bool,ocapAlways :bool): print('Creating test server: Eve on port '+str(port)) if os.path.isdir(path): shutil.rmtree(path) @@ -180,7 +183,7 @@ def createServerEve(path: str,domain: str,port: int,federationList: [],ocapGrant global testServerEveRunning testServerEveRunning = True print('Server running: Eve') - runDaemon(path,domain,port,httpPrefix,federationList,ocapAlways,ocapGranted,useTor,True) + runDaemon(path,domain,port,httpPrefix,federationList,ocapAlways,useTor,True) def testPostMessageBetweenServers(): print('Testing sending message from one server to the inbox of another') @@ -193,7 +196,6 @@ def testPostMessageBetweenServers(): httpPrefix='http' useTor=False federationList=['127.0.0.50','127.0.0.100'] - ocapGranted={} baseDir=os.getcwd() if os.path.isdir(baseDir+'/.tests'): @@ -206,12 +208,12 @@ def testPostMessageBetweenServers(): aliceDir=baseDir+'/.tests/alice' aliceDomain='127.0.0.50' alicePort=61935 - thrAlice = threadWithTrace(target=createServerAlice,args=(aliceDir,aliceDomain,alicePort,federationList,ocapGranted,True,True,ocapAlways),daemon=True) + thrAlice = threadWithTrace(target=createServerAlice,args=(aliceDir,aliceDomain,alicePort,federationList,True,True,ocapAlways),daemon=True) bobDir=baseDir+'/.tests/bob' bobDomain='127.0.0.100' bobPort=61936 - thrBob = threadWithTrace(target=createServerBob,args=(bobDir,bobDomain,bobPort,federationList,ocapGranted,True,True,ocapAlways),daemon=True) + thrBob = threadWithTrace(target=createServerBob,args=(bobDir,bobDomain,bobPort,federationList,True,True,ocapAlways),daemon=True) thrAlice.start() thrBob.start() @@ -238,7 +240,7 @@ def testPostMessageBetweenServers(): ccUrl=None alicePersonCache={} aliceCachedWebfingers={} - sendResult = sendPost(sessionAlice,aliceDir,'alice', aliceDomain, alicePort, 'bob', bobDomain, bobPort, ccUrl, httpPrefix, 'Why is a mouse when it spins?', followersOnly, saveToFile, clientToServer, federationList, ocapGranted, aliceSendThreads, alicePostLog, aliceCachedWebfingers,alicePersonCache,inReplyTo, inReplyToAtomUri, subject) + sendResult = sendPost(sessionAlice,aliceDir,'alice', aliceDomain, alicePort, 'bob', bobDomain, bobPort, ccUrl, httpPrefix, 'Why is a mouse when it spins?', followersOnly, saveToFile, clientToServer, federationList, aliceSendThreads, alicePostLog, aliceCachedWebfingers,alicePersonCache,inReplyTo, inReplyToAtomUri, subject) print('sendResult: '+str(sendResult)) queuePath=bobDir+'/accounts/bob@'+bobDomain+'/queue' @@ -280,7 +282,6 @@ def testFollowBetweenServers(): httpPrefix='http' useTor=False federationList=[] - ocapGranted={} baseDir=os.getcwd() if os.path.isdir(baseDir+'/.tests'): @@ -293,17 +294,17 @@ def testFollowBetweenServers(): aliceDir=baseDir+'/.tests/alice' aliceDomain='127.0.0.42' alicePort=61935 - thrAlice = threadWithTrace(target=createServerAlice,args=(aliceDir,aliceDomain,alicePort,federationList,ocapGranted,False,False,ocapAlways),daemon=True) + thrAlice = threadWithTrace(target=createServerAlice,args=(aliceDir,aliceDomain,alicePort,federationList,False,False,ocapAlways),daemon=True) bobDir=baseDir+'/.tests/bob' bobDomain='127.0.0.64' bobPort=61936 - thrBob = threadWithTrace(target=createServerBob,args=(bobDir,bobDomain,bobPort,federationList,ocapGranted,False,False,ocapAlways),daemon=True) + thrBob = threadWithTrace(target=createServerBob,args=(bobDir,bobDomain,bobPort,federationList,False,False,ocapAlways),daemon=True) eveDir=baseDir+'/.tests/eve' eveDomain='127.0.0.55' evePort=61937 - thrEve = threadWithTrace(target=createServerEve,args=(eveDir,eveDomain,evePort,federationList,ocapGranted,False,False,False),daemon=True) + thrEve = threadWithTrace(target=createServerEve,args=(eveDir,eveDomain,evePort,federationList,False,False,False),daemon=True) thrAlice.start() thrBob.start() @@ -326,7 +327,8 @@ def testFollowBetweenServers(): time.sleep(1) # In the beginning all was calm and there were no follows - + + print('*********************************************************') print('Alice sends a follow request to Bob') print('Both are strictly enforcing object capabilities') os.chdir(aliceDir) @@ -348,20 +350,31 @@ def testFollowBetweenServers(): sendFollowRequest(sessionAlice,aliceDir, \ 'alice',aliceDomain,alicePort,httpPrefix, \ 'bob',bobDomain,bobPort,httpPrefix, \ - clientToServer,federationList,ocapGranted, + clientToServer,federationList, aliceSendThreads,alicePostLog, \ aliceCachedWebfingers,alicePersonCache,True) print('sendResult: '+str(sendResult)) + bobCapsFilename=bobDir+'/accounts/bob@'+bobDomain+'/ocap/accept/'+httpPrefix+':##'+aliceDomain+':'+str(alicePort)+'#users#alice.json' + aliceCapsFilename=aliceDir+'/accounts/alice@'+aliceDomain+'/ocap/granted/'+httpPrefix+':##'+bobDomain+':'+str(bobPort)+'#users#bob.json' + for t in range(10): if os.path.isfile(bobDir+'/accounts/bob@'+bobDomain+'/followers.txt'): if os.path.isfile(aliceDir+'/accounts/alice@'+aliceDomain+'/following.txt'): - if os.path.isfile(bobDir+'/accounts/bob@'+bobDomain+'/ocap/accept/'+httpPrefix+':##'+aliceDomain+':'+str(alicePort)+'#users#alice.json'): - if os.path.isfile(aliceDir+'/accounts/alice@'+aliceDomain+'/ocap/granted/'+httpPrefix+':##'+bobDomain+':'+str(bobPort)+'#users#bob.json'): + if os.path.isfile(bobCapsFilename): + if os.path.isfile(aliceCapsFilename): break time.sleep(1) + + with open(bobCapsFilename, 'r') as fp: + bobCapsJson=commentjson.load(fp) + if not bobCapsJson.get('capability'): + print("Unexpected format for Bob's capabilities") + pprint(bobCapsJson) + assert False - print('\n\nEve tries to send to Bob') + print('\n\n*********************************************************') + print('Eve tries to send to Bob') sessionEve = createSession(eveDomain,evePort,useTor) eveSendThreads = [] evePostLog = [] @@ -369,7 +382,7 @@ def testFollowBetweenServers(): eveCachedWebfingers={} eveSendThreads=[] evePostLog=[] - sendResult = sendPost(sessionEve,eveDir,'eve', eveDomain, evePort, 'bob', bobDomain, bobPort, ccUrl, httpPrefix, 'Eve message', followersOnly, saveToFile, clientToServer, federationList, ocapGranted, eveSendThreads, evePostLog, eveCachedWebfingers,evePersonCache,inReplyTo, inReplyToAtomUri, subject) + sendResult = sendPost(sessionEve,eveDir,'eve', eveDomain, evePort, 'bob', bobDomain, bobPort, ccUrl, httpPrefix, 'Eve message', followersOnly, saveToFile, clientToServer, federationList, eveSendThreads, evePostLog, eveCachedWebfingers,evePersonCache,inReplyTo, inReplyToAtomUri, subject) print('sendResult: '+str(sendResult)) queuePath=bobDir+'/accounts/bob@'+bobDomain+'/queue' @@ -387,14 +400,15 @@ def testFollowBetweenServers(): assert eveMessageArrived==False print('Message from Eve to Bob was correctly rejected by object capabilities') - + print('\n\n*********************************************************') + print('Alice sends a message to Bob') aliceSendThreads = [] alicePostLog = [] alicePersonCache={} aliceCachedWebfingers={} aliceSendThreads=[] alicePostLog=[] - sendResult = sendPost(sessionAlice,aliceDir,'alice', aliceDomain, alicePort, 'bob', bobDomain, bobPort, ccUrl, httpPrefix, 'Alice message', followersOnly, saveToFile, clientToServer, federationList, ocapGranted, aliceSendThreads, alicePostLog, aliceCachedWebfingers,alicePersonCache,inReplyTo, inReplyToAtomUri, subject) + sendResult = sendPost(sessionAlice,aliceDir,'alice', aliceDomain, alicePort, 'bob', bobDomain, bobPort, ccUrl, httpPrefix, 'Alice message', followersOnly, saveToFile, clientToServer, federationList, aliceSendThreads, alicePostLog, aliceCachedWebfingers,alicePersonCache,inReplyTo, inReplyToAtomUri, subject) print('sendResult: '+str(sendResult)) queuePath=bobDir+'/accounts/bob@'+bobDomain+'/queue' @@ -411,6 +425,64 @@ def testFollowBetweenServers(): assert aliceMessageArrived==True print('Message from Alice to Bob succeeded, since it was granted capabilities') + print('\n\n*********************************************************') + print("\nBob changes Alice's capabilities so that she can't reply on his posts") + sessionBob = createSession(bobDomain,bobPort,useTor) + bobSendThreads = [] + bobPostLog = [] + bobPersonCache={} + bobCachedWebfingers={} + print("Bob's capabilities for Alice:") + with open(bobCapsFilename, 'r') as fp: + bobCapsJson=commentjson.load(fp) + pprint(bobCapsJson) + assert "inbox:noreply" not in bobCapsJson['capability'] + print("Alice's capabilities granted by Bob") + with open(aliceCapsFilename, 'r') as fp: + aliceCapsJson=commentjson.load(fp) + pprint(aliceCapsJson) + assert "inbox:noreply" not in aliceCapsJson['capability'] + newCapabilities=["inbox:write","objects:read","inbox:noreply"] + sendCapabilitiesUpdate(sessionBob,bobDir,httpPrefix, \ + 'bob',bobDomain,bobPort, \ + httpPrefix+'://'+aliceDomain+':'+str(alicePort)+'/users/alice', + newCapabilities, \ + bobSendThreads, bobPostLog, \ + bobCachedWebfingers,bobPersonCache, \ + federationList,True) + + bobChanged=False + bobNewCapsJson=None + for i in range(20): + time.sleep(1) + with open(bobCapsFilename, 'r') as fp: + bobNewCapsJson=commentjson.load(fp) + if "inbox:noreply" in bobNewCapsJson['capability']: + print("Bob's capabilities were changed") + pprint(bobNewCapsJson) + bobChanged=True + break + + assert bobChanged + + aliceChanged=False + aliceNewCapsJson=None + for i in range(20): + time.sleep(1) + with open(aliceCapsFilename, 'r') as fp: + aliceNewCapsJson=commentjson.load(fp) + if "inbox:noreply" in aliceNewCapsJson['capability']: + print("Alice's granted capabilities were changed") + pprint(aliceNewCapsJson) + aliceChanged=True + break + + assert aliceChanged + + # check that the capabilities id has changed + assert bobNewCapsJson['id']!=bobCapsJson['id'] + assert aliceNewCapsJson['id']!=aliceCapsJson['id'] + # stop the servers thrAlice.kill() thrAlice.join() diff --git a/utils.py b/utils.py index 3a0a969d2..ad80f108d 100644 --- a/utils.py +++ b/utils.py @@ -48,7 +48,7 @@ def domainPermitted(domain: str, federationList: []): return True return False -def urlPermitted(url: str, federationList: [],ocapGranted: {},capability: str): +def urlPermitted(url: str, federationList: [],capability: str): if len(federationList)==0: return True for domain in federationList: