diff --git a/acceptreject.py b/acceptreject.py index 4da82e724..ca892aba8 100644 --- a/acceptreject.py +++ b/acceptreject.py @@ -8,6 +8,8 @@ __status__ = "Production" import json import commentjson +from capabilities import capabilitiesAccept +from capabilities import capabilitiesGrantedSave from utils import getStatusNumber from utils import createOutboxDir from utils import urlPermitted @@ -16,10 +18,10 @@ from utils import getNicknameFromActor from utils import domainPermitted from utils import followPerson -def createAcceptReject(baseDir: str,federationList: [],capsList: [], \ +def createAcceptReject(baseDir: str,federationList: [],ocapGranted: {}, \ nickname: str,domain: str,port: int, \ toUrl: str,ccUrl: str,httpPrefix: str, \ - objectJson: {},acceptType: str) -> {}: + objectJson: {},ocapJson,acceptType: str) -> {}: """Accepts or rejects something (eg. a follow request or offer) Typically toUrl will be https://www.w3.org/ns/activitystreams#Public and ccUrl might be a specific person favorited or repeated and @@ -29,7 +31,7 @@ def createAcceptReject(baseDir: str,federationList: [],capsList: [], \ if not objectJson.get('actor'): return None - if not urlPermitted(objectJson['actor'],federationList,capsList,"inbox:write"): + if not urlPermitted(objectJson['actor'],federationList,ocapGranted,"inbox:write"): return None if port!=80 and port!=443: @@ -45,28 +47,33 @@ def createAcceptReject(baseDir: str,federationList: [],capsList: [], \ if ccUrl: if len(ccUrl)>0: newAccept['cc']=ccUrl + # attach capabilities for follow accept + if ocapJson: + newAccept['capabilities']=ocapJson return newAccept -def createAccept(baseDir: str,federationList: [],capsList: [], \ +def createAccept(baseDir: str,federationList: [],ocapGranted: {}, \ nickname: str,domain: str,port: int, \ toUrl: str,ccUrl: str,httpPrefix: str, \ objectJson: {}) -> {}: - return createAcceptReject(baseDir,federationList,capsList, \ + # create capabilities accept + ocapNew=capabilitiesAccept(baseDir,httpPrefix,nickname,domain, toUrl, True) + return createAcceptReject(baseDir,federationList,ocapGranted, \ nickname,domain,port, \ toUrl,ccUrl,httpPrefix, \ - objectJson,'Accept') + objectJson,ocapNew,'Accept') -def createReject(baseDir: str,federationList: [],capsList: [], \ +def createReject(baseDir: str,federationList: [],ocapGranted: {}, \ nickname: str,domain: str,port: int, \ toUrl: str,ccUrl: str,httpPrefix: str, \ objectJson: {}) -> {}: - return createAcceptReject(baseDir,federationList,capsList, \ + return createAcceptReject(baseDir,federationList,ocapGranted, \ nickname,domain,port, \ toUrl,ccUrl, \ - httpPrefix,objectJson,'Reject') + httpPrefix,objectJson,None,'Reject') def acceptFollow(baseDir: str,domain : str,messageJson: {}, \ - federationList: [],capsList: [],debug : bool) -> None: + federationList: [],ocapGranted: {},debug : bool) -> None: if not messageJson.get('object'): return if not messageJson['object'].get('type'): @@ -115,6 +122,12 @@ def acceptFollow(baseDir: str,domain : str,messageJson: {}, \ followedNickname=getNicknameFromActor(followedActor) if not followedNickname: return + + # are capabilities attached? If so then store them + if messageJson['object'].get('capabilities'): + if isinstance(messageJson['object']['capabilities'], dict): + capabilitiesGrantedSave(baseDir,messageJson['object']['capabilities']) + if followPerson(baseDir,nickname,domain, \ followedNickname,followedDomain, \ federationList,debug): @@ -128,7 +141,7 @@ def receiveAcceptReject(session,baseDir: str, \ httpPrefix: str,domain :str,port: int, \ sendThreads: [],postLog: [],cachedWebfingers: {}, \ personCache: {},messageJson: {},federationList: [], \ - capsList: [],debug : bool) -> bool: + ocapGranted: {},debug : bool) -> bool: """Receives an Accept or Reject within the POST section of HTTPServer """ if messageJson['type']!='Accept' and messageJson['type']!='Reject': @@ -152,7 +165,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,capsList,debug) + acceptFollow(baseDir,domain,messageJson,federationList,ocapGranted,debug) if debug: print('DEBUG: Uh, '+messageJson['type']+', I guess') return True diff --git a/announce.py b/announce.py index 7a8baaff1..6ee63ecee 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: [], capsList: [], \ +def createAnnounce(baseDir: str,federationList: [], ocapGranted: {}, \ 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: [], capsList: [], \ followers url objectUrl is typically the url of the message, corresponding to url or atomUri in createPostBase """ - if not urlPermitted(objectUrl,federationList,capsList,"inbox:write"): + if not urlPermitted(objectUrl,federationList,ocapGranted,"inbox:write"): return None if port!=80 and port!=443: diff --git a/capabilities.py b/capabilities.py index 5ef558161..83fe79949 100644 --- a/capabilities.py +++ b/capabilities.py @@ -7,42 +7,99 @@ __email__ = "bob@freedombone.net" __status__ = "Production" import os +import datetime +import time +import json +import commentjson from auth import createPassword -def sendCapabilitiesRequest(baseDir: str,httpPrefix: str,domain: str, \ - requestedActor: str, \ - requestedCaps=["inbox:write","objects:read"]) -> None: +def capabilitiesMakeDirs(baseDir: str): + if not os.path.isdir(baseDir+'/ocap'): + os.mkdir(baseDir+'/ocap') + # for capabilities accepted by this instance + if not os.path.isdir(baseDir+'/ocap/accept'): + os.mkdir(baseDir+'/ocap/accept') + # for capabilities granted to this instance + if not os.path.isdir(baseDir+'/ocap/granted'): + os.mkdir(baseDir+'/ocap/granted') + +def capabilitiesRequest(baseDir: str,httpPrefix: str,domain: str, \ + requestedActor: str, \ + requestedCaps=["inbox:write","objects:read"]) -> {}: # This is sent to the capabilities endpoint /caps/new # which could be instance wide or for a particular person - capId=createPassword(32) - capRequest = { - "id": httpPrefix+"://"+requestedDomain+"/caps/request/"+capId, + # This could also be added to a follow activity + capabilitiesMakeDirs(baseDir) + + ocapId=createPassword(32) + ocapRequest = { + "id": httpPrefix+"://"+requestedDomain+"/caps/request/"+ocapId, "type": "Request", "capability": requestedCaps, "actor": requestedActor } - #TODO + return ocapRequest -def sendCapabilitiesAccept(baseDir: str,httpPrefix: str,nickname: str,domain: str, \ - acceptedActor: str, \ - acceptedCaps=["inbox:write","objects:read"]) -> None: +def capabilitiesAccept(baseDir: str,httpPrefix: str,nickname: str,domain: str, \ + acceptedActor: str, saveToFile: bool, \ + acceptedCaps=["inbox:write","objects:read"]) -> {}: # This gets returned to capabilities requester - capId=createPassword(32) - capAccept = { - "id": httpPrefix+"://"+domain+"/caps/"+capId, - "type": "Capability", - "capability": acceptedCaps, - "scope": acceptedActor, - "actor": httpPrefix+"://"+domain - } - if nickname: - capAccept['actor']=httpPrefix+"://"+domain+'/users/'+nickname - #TODO + # This could also be added to a follow Accept activity -def isCapable(actor: str,capsJson: [],capability: str) -> bool: + # reject excessively long actors + if len(acceptedActor)>256: + return None + + # make directories to store capabilities + capabilitiesMakeDirs(baseDir) + filename=baseDir+'/ocap/accept/'+acceptedActor.replace('/','#')+'.json' + ocapAccept=None + + # if the capability already exists then load it from file + if os.path.isfile(filename): + with open(filename, 'r') as fp: + ocapAccept=commentjson.load(fp) + # otherwise create a new capability + if not ocapAccept: + ocapId=createPassword(32) + ocapAccept = { + "id": httpPrefix+"://"+domain+"/caps/"+ocapId, + "type": "Capability", + "capability": acceptedCaps, + "scope": acceptedActor, + "actor": httpPrefix+"://"+domain + } + if nickname: + ocapAccept['actor']=httpPrefix+"://"+domain+'/users/'+nickname + + if saveToFile: + with open(filename, 'w') as fp: + commentjson.dump(ocapAccept, fp, indent=4, sort_keys=False) + return ocapAccept + +def capabilitiesGrantedSave(baseDir :str,ocap: {}) -> bool: + """A capabilities accept is received, so stor it for + reference when sending to the actor + """ + if not ocap.get('actor'): + return False + filename=baseDir+'/ocap/granted/'+ocap['actor'].replace('/','#')+'.json' + with open(filename, 'w') as fp: + commentjson.dump(ocap, fp, indent=4, sort_keys=False) + return True + +def isCapable(actor: str,ocapGranted: {},capability: str) -> bool: # is the given actor capable of using the current resource? - for cap in capsJson: - if cap['scope'] in actor: - if capability in cap['capability']: + for id,ocap in ocapGranted.items(): + if ocap['scope'] in actor: + if capability in ocap['capability']: + return True + return False + +def isCapableId(id: str,ocapGranted: {},capability: str) -> bool: + # is the given id capable of using the current resource? + if ocapGranted.get(id): + if ocapGranted['id']['scope'] in actor: + if capability in ocapGranted['id']['capability']: return True return False diff --git a/daemon.py b/daemon.py index adbd7b640..77a5e0ac4 100644 --- a/daemon.py +++ b/daemon.py @@ -451,7 +451,7 @@ class PubServer(BaseHTTPRequestHandler): if not inboxPermittedMessage(self.server.domain, \ messageJson, \ self.server.federationList, \ - self.server.capsList): + self.server.ocapGranted): if self.server.debug: # https://www.youtube.com/watch?v=K3PrSj9XEu4 print('DEBUG: Ah Ah Ah') @@ -498,7 +498,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy=False def runDaemon(baseDir: str,domain: str,port=80,httpPrefix='https', \ - fedList=[],capsList=[],useTor=False,debug=False) -> None: + fedList=[],ocapGranted={},useTor=False,debug=False) -> None: if len(domain)==0: domain='localhost' if '.' not in domain: @@ -513,7 +513,7 @@ def runDaemon(baseDir: str,domain: str,port=80,httpPrefix='https', \ httpd.httpPrefix=httpPrefix httpd.debug=debug httpd.federationList=fedList.copy() - httpd.capsList=capsList.copy() + httpd.ocapGranted=ocapGranted.copy() httpd.baseDir=baseDir httpd.personCache={} httpd.cachedWebfingers={} @@ -535,6 +535,6 @@ def runDaemon(baseDir: str,domain: str,port=80,httpPrefix='https', \ httpd.postLog,httpd.cachedWebfingers, \ httpd.personCache,httpd.inboxQueue, \ domain,port,useTor,httpd.federationList, \ - httpd.capsList,debug),daemon=True) + httpd.ocapGranted,debug),daemon=True) httpd.thrInboxQueue.start() httpd.serve_forever() diff --git a/epicyon.py b/epicyon.py index b4a93f1fe..8e8b183cb 100644 --- a/epicyon.py +++ b/epicyon.py @@ -6,6 +6,7 @@ __maintainer__ = "Bob Mottram" __email__ = "bob@freedombone.net" __status__ = "Production" +from capabilities import capabilitiesMakeDirs from person import createPerson from person import createSharedInbox from person import createCapabilitiesInbox @@ -314,7 +315,7 @@ else: if configFederationList: federationList=configFederationList -capsList=[] +ocapGranted={} if federationList: print('Federating with: '+str(federationList)) @@ -334,6 +335,8 @@ if not os.path.isdir(baseDir+'/accounts/capabilities@'+domain): print('Creating capabilities account which can sign requests') createCapabilitiesInbox(baseDir,'capabilities',domain,port,httpPrefix) +capabilitiesMakeDirs(baseDir) + if args.testdata: nickname='testuser567' print('Generating some test data for user: '+nickname) @@ -342,13 +345,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,capsList) - createPublicPost(baseDir,nickname,domain,port,httpPrefix,"Zoiks!!!",False,True,False,capsList) - createPublicPost(baseDir,nickname,domain,port,httpPrefix,"Hey scoob we need like a hundred more milkshakes",False,True,False,capsList) - createPublicPost(baseDir,nickname,domain,port,httpPrefix,"Getting kinda spooky around here",False,True,False,capsList) - 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,capsList) - createPublicPost(baseDir,nickname,domain,port,httpPrefix,"man, these centralized sites are, like, the worst!",False,True,False,capsList) - createPublicPost(baseDir,nickname,domain,port,httpPrefix,"another mystery solved hey",False,True,False,capsList) - createPublicPost(baseDir,nickname,domain,port,httpPrefix,"let's go bowling",False,True,False,capsList) + 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) -runDaemon(baseDir,domain,port,httpPrefix,federationList,capsList,useTor,debug) +runDaemon(baseDir,domain,port,httpPrefix,federationList,ocapGranted,useTor,debug) diff --git a/follow.py b/follow.py index c20950223..1e13dcf6b 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: [], \ - capsList: [],debug : bool) -> bool: + ocapGranted: {},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,capsList, \ + acceptJson=createAccept(baseDir,federationList,ocapGranted, \ nickname,domain,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, capsList, \ + federationList, ocapGranted, \ 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: [],capsList: [], \ + clientToServer: bool,federationList: [],ocapGranted: {}, \ sendThreads: [],postLog: [],cachedWebfingers: {}, \ personCache: {},debug : bool) -> {}: """Gets the json object for sending a follow request @@ -320,8 +320,8 @@ def sendFollowRequest(session,baseDir: str, \ requestDomain=followDomain+':'+str(followPort) # check that we are capable - if capsList: - if not isCapable(followActor,capsList,'inbox:write'): + if ocapGranted: + if not isCapable(followActor,ocapGranted,'inbox:write'): return None statusNumber,published = getStatusNumber() @@ -342,7 +342,7 @@ def sendFollowRequest(session,baseDir: str, \ followNickname,followDomain,followPort, \ 'https://www.w3.org/ns/activitystreams#Public', \ httpPrefix,True,clientToServer, \ - federationList, capsList, \ + federationList, ocapGranted, \ sendThreads,postLog,cachedWebfingers,personCache, debug) return newFollowJson diff --git a/inbox.py b/inbox.py index eb6053011..acbdf7536 100644 --- a/inbox.py +++ b/inbox.py @@ -65,7 +65,7 @@ def inboxMessageHasParams(messageJson: {}) -> bool: return False return True -def inboxPermittedMessage(domain: str,messageJson: {},federationList: [],capsList: []) -> bool: +def inboxPermittedMessage(domain: str,messageJson: {},federationList: [],ocapGranted: {}) -> bool: """ check that we are receiving from a permitted domain """ testParam='actor' @@ -76,14 +76,14 @@ def inboxPermittedMessage(domain: str,messageJson: {},federationList: [],capsLis if domain in actor: return True - if not urlPermitted(actor,federationList,capsList,"inbox:write"): + if not urlPermitted(actor,federationList,ocapGranted,"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,capsList): + if not urlPermitted(inReplyTo,federationList,ocapGranted): return False return True @@ -144,7 +144,7 @@ def savePostToInboxQueue(baseDir: str,httpPrefix: str,nickname: str, domain: str commentjson.dump(newQueueItem, fp, indent=4, sort_keys=False) return filename -def runInboxQueue(baseDir: str,httpPrefix: str,sendThreads: [],postLog: [],cachedWebfingers: {},personCache: {},queue: [],domain: str,port: int,useTor: bool,federationList: [],capsList: [],debug: bool) -> None: +def runInboxQueue(baseDir: str,httpPrefix: str,sendThreads: [],postLog: [],cachedWebfingers: {},personCache: {},queue: [],domain: str,port: int,useTor: bool,federationList: [],ocapGranted: {},debug: bool) -> None: """Processes received items and moves them to the appropriate directories """ @@ -174,6 +174,57 @@ def runInboxQueue(baseDir: str,httpPrefix: str,sendThreads: [],postLog: [],cache with open(queueFilename, 'r') as fp: queueJson=commentjson.load(fp) + # check that capabilities are accepted + if queueJson['post'].get('capabilities'): + if queueJson['post']['type']!='Accept': + if isinstance(queueJson['post']['capabilities'], dict): + if debug: + print('DEBUG: received post capabilities should be a string, not a dict') + pprint(queueJson['post']) + os.remove(queueFilename) + queue.pop(0) + continue + if not queueJson['post'].get('actor'): + if debug: + print('DEBUG: post should have an actor') + os.remove(queueFilename) + queue.pop(0) + continue + ocapFilename=baseDir+'/ocap/accept/'+queueJson['post']['actor'].replace('/','#')+'.json' + if not os.path.isfile(ocapFilename): + if debug: + print('DEBUG: capabilities for '+queueJson['post']['actor']+' do not exist') + os.remove(queueFilename) + queue.pop(0) + continue + with open(ocapFilename, 'r') as fp: + oc=commentjson.load(fp) + if not oc.get('id'): + if debug: + print('DEBUG: capabilities for '+queueJson['post']['actor']+' do not contain an id') + os.remove(queueFilename) + queue.pop(0) + continue + if oc['id']!=queueJson['post']['capabilities']: + if debug: + print('DEBUG: capabilities id mismatch') + os.remove(queueFilename) + queue.pop(0) + continue + if not queueJson['post']['capabilities'].get('capability'): + if debug: + print('DEBUG: missing capability list') + os.remove(queueFilename) + queue.pop(0) + continue + if 'inbox:write' not in queueJson['post']['capabilities']['capability']: + if debug: + print('DEBUG: insufficient capabilities to write to inbox from '+ \ + queueJson['post']['actor']) + os.remove(queueFilename) + queue.pop(0) + continue + # Try a few times to obtain the public key pubKey=None keyId=None @@ -232,7 +283,7 @@ def runInboxQueue(baseDir: str,httpPrefix: str,sendThreads: [],postLog: [],cache cachedWebfingers, personCache, queueJson['post'], \ - federationList,capsList, \ + federationList,ocapGranted, \ debug): if debug: print('DEBUG: Follow accepted from '+keyId) @@ -246,7 +297,7 @@ def runInboxQueue(baseDir: str,httpPrefix: str,sendThreads: [],postLog: [],cache cachedWebfingers, personCache, queueJson['post'], \ - federationList,capsList, \ + federationList,ocapGranted, \ debug): if debug: print('DEBUG: Accept/Reject received from '+keyId) diff --git a/like.py b/like.py index 2584e60ce..17552ded8 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,capsList,"inbox:write"): + if not urlPermitted(objectUrl,federationList,ocapGranted,"inbox:write"): return None if port!=80 and port!=443: diff --git a/posts.py b/posts.py index 54b2a25c1..fa13c046a 100644 --- a/posts.py +++ b/posts.py @@ -153,7 +153,7 @@ def getPersonBox(session,wfRequest: {},personCache: {}, \ def getPosts(session,outboxUrl: str,maxPosts: int,maxMentions: int, \ maxEmoji: int,maxAttachments: int, \ - federationList: [], capsList: [],\ + federationList: [], ocapGranted: {},\ personCache: {},raw: bool,simple: bool) -> {}: personPosts={} if not outboxUrl: @@ -193,7 +193,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,capsList, \ + federationList,ocapGranted, \ "objects:read"): emojiName=tagItem['name'] emojiIcon=tagItem['icon']['url'] @@ -217,7 +217,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,capsList, \ + federationList,ocapGranted, \ "objects:read"): continue inReplyTo = item['object']['inReplyTo'] @@ -227,7 +227,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,capsList,"objects:read"): + federationList,ocapGranted,"objects:read"): conversation = item['object']['conversation'] attachment = [] @@ -237,7 +237,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,capsList, \ + federationList,ocapGranted, \ "objects:read"): attachment.append([attach['name'],attach['url']]) @@ -319,7 +319,7 @@ 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, \ - capsList: [], \ + ocapGranted: {}, \ inReplyTo=None, inReplyToAtomUri=None, subject=None) -> {}: """Creates a message """ @@ -335,8 +335,7 @@ def createPostBase(baseDir: str,nickname: str, domain: str, port: int, \ postTo=postCC postCC='' newPostId=httpPrefix+'://'+domain+'/users/'+nickname+'/statuses/'+statusNumber - # TODO - capabilityUrl='' + sensitive=False summary=None if subject: @@ -344,13 +343,24 @@ def createPostBase(baseDir: str,nickname: str, domain: str, port: int, \ sensitive=True if not clientToServer: actorUrl=httpPrefix+'://'+domain+'/users/'+nickname - if capsList: - if not isCapable(actorUrl,capsList,'inbox:write'): + if ocapGranted: + if not isCapable(actorUrl,ocapGranted,'inbox:write'): return None + # if capabilities have been granted for this actor + # then get the corresponding id + capabilityId=None + ocapFilename= \ + baseDir+'/ocap/granted/'+actorUrl.replace('/','#')+'.json' + if os.path.isfile(ocapFilename): + with open(ocapFilename, 'r') as fp: + oc=commentjson.load(fp) + if oc.get('id'): + capabilityId=oc['id'] + newPost = { 'id': newPostId+'/activity', - 'capability': capabilityUrl, + 'capability': capabilityId, 'type': 'Create', 'actor': actorUrl, 'published': published, @@ -454,7 +464,7 @@ def outboxMessageCreateWrap(httpPrefix: str,nickname: str,domain: str, \ def createPublicPost(baseDir: str, nickname: str, domain: str, port: int,httpPrefix: str, \ content: str, followersOnly: bool, saveToFile: bool, - clientToServer: bool, capsList: [],\ + clientToServer: bool, ocapGranted: {},\ inReplyTo=None, inReplyToAtomUri=None, subject=None) -> {}: """Public post to the outbox """ @@ -462,10 +472,10 @@ def createPublicPost(baseDir: str, 'https://www.w3.org/ns/activitystreams#Public', \ httpPrefix+'://'+domain+'/users/'+nickname+'/followers', \ httpPrefix, content, followersOnly, saveToFile, \ - clientToServer, capsList, \ + clientToServer, ocapGranted, \ inReplyTo, inReplyToAtomUri, subject) -def threadSendPost(session,postJsonObject: {},federationList: [],capsList: [],\ +def threadSendPost(session,postJsonObject: {},federationList: [],ocapGranted: {},\ inboxUrl: str, baseDir: str,signatureHeaderJson: {},postLog: [], debug :bool) -> None: """Sends a post with exponential backoff @@ -474,7 +484,7 @@ def threadSendPost(session,postJsonObject: {},federationList: [],capsList: [],\ backoffTime=60 for attempt in range(20): postResult = postJson(session,postJsonObject,federationList, \ - capsList,inboxUrl,signatureHeaderJson, \ + ocapGranted,inboxUrl,signatureHeaderJson, \ "inbox:write") if postResult: if debug: @@ -501,7 +511,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: [], capsList: [],\ + federationList: [], ocapGranted: {},\ sendThreads: [], postLog: [], cachedWebfingers: {},personCache: {}, \ debug=False,inReplyTo=None,inReplyToAtomUri=None,subject=None) -> int: """Post to another inbox @@ -550,7 +560,7 @@ def sendPost(session,baseDir: str,nickname: str, domain: str, port: int, \ createPostBase(baseDir,nickname,domain,port, \ toPersonId,cc,httpPrefix,content, \ followersOnly,saveToFile,clientToServer, \ - capsList, \ + ocapGranted, \ inReplyTo,inReplyToAtomUri,subject) # get the senders private key @@ -574,7 +584,7 @@ def sendPost(session,baseDir: str,nickname: str, domain: str, port: int, \ thr = threadWithTrace(target=threadSendPost,args=(session, \ postJsonObject.copy(), \ federationList, \ - capsList, \ + ocapGranted, \ inboxUrl,baseDir, \ signatureHeaderJson.copy(), \ postLog, @@ -587,7 +597,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: [], capsList: [], \ + federationList: [], ocapGranted: {}, \ sendThreads: [], postLog: [], cachedWebfingers: {}, \ personCache: {}, debug: bool) -> int: """Sends a signed json object to an inbox/outbox @@ -661,7 +671,7 @@ def sendSignedJson(postJsonObject: {},session,baseDir: str, \ args=(session, \ postJsonObject.copy(), \ federationList, \ - capsList, \ + ocapGranted, \ inboxUrl,baseDir, \ signatureHeaderJson.copy(), \ postLog, @@ -812,7 +822,7 @@ def getPublicPostsOfPerson(nickname: str,domain: str, \ personCache={} cachedWebfingers={} federationList=[] - capsList=[] + ocapGranted={} httpPrefix='https' handle=httpPrefix+"://"+domain+"/@"+nickname @@ -829,6 +839,6 @@ def getPublicPostsOfPerson(nickname: str,domain: str, \ maxEmoji=10 maxAttachments=5 userPosts = getPosts(session,personUrl,30,maxMentions,maxEmoji, \ - maxAttachments,federationList,capsList, \ + maxAttachments,federationList,ocapGranted, \ personCache,raw,simple) #print(str(userPosts)) diff --git a/session.py b/session.py index d30318ff7..a3603576b 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: [],capsList: [],inboxUrl: str,headers: {},capability: str) -> str: +def postJson(session,postJsonObject: {},federationList: [],ocapGranted: {},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: [],capsList: [],inboxUrl # always allow capability requests if not capability.startswith('cap'): # check that we are posting to a permitted domain - if not urlPermitted(inboxUrl,federationList,capsList,capability): + if not urlPermitted(inboxUrl,federationList,ocapGranted,capability): return None postResult = session.post(url = inboxUrl, data = json.dumps(postJsonObject), headers=headers) diff --git a/tests.py b/tests.py index f0e221053..b9fca914f 100644 --- a/tests.py +++ b/tests.py @@ -108,7 +108,7 @@ def testThreads(): thr.join() assert thr.isAlive()==False -def createServerAlice(path: str,domain: str,port: int,federationList: [],capsList: [],hasFollows: bool,hasPosts :bool): +def createServerAlice(path: str,domain: str,port: int,federationList: [],ocapGranted: {},hasFollows: bool,hasPosts :bool): print('Creating test server: Alice on port '+str(port)) if os.path.isdir(path): shutil.rmtree(path) @@ -126,15 +126,15 @@ def createServerAlice(path: str,domain: str,port: int,federationList: [],capsLis 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,capsList) - createPublicPost(path,nickname, domain, port,httpPrefix, "Curiouser and curiouser!", False, True, clientToServer,capsList) - 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,capsList) + 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) global testServerAliceRunning testServerAliceRunning = True print('Server running: Alice') - runDaemon(path,domain,port,httpPrefix,federationList,capsList,useTor,True) + runDaemon(path,domain,port,httpPrefix,federationList,ocapGranted,useTor,True) -def createServerBob(path: str,domain: str,port: int,federationList: [],capsList: [],hasFollows: bool,hasPosts :bool): +def createServerBob(path: str,domain: str,port: int,federationList: [],ocapGranted: {},hasFollows: bool,hasPosts :bool): print('Creating test server: Bob on port '+str(port)) if os.path.isdir(path): shutil.rmtree(path) @@ -152,13 +152,13 @@ def createServerBob(path: str,domain: str,port: int,federationList: [],capsList: 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,capsList) - createPublicPost(path,nickname, domain, port,httpPrefix, "One of the things I've realised is that I am very simple", False, True, clientToServer,capsList) - createPublicPost(path,nickname, domain, port,httpPrefix, "Quantum physics is a bit of a passion of mine", False, True, clientToServer,capsList) + 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) global testServerBobRunning testServerBobRunning = True print('Server running: Bob') - runDaemon(path,domain,port,httpPrefix,federationList,capsList,useTor,True) + runDaemon(path,domain,port,httpPrefix,federationList,ocapGranted,useTor,True) def testPostMessageBetweenServers(): print('Testing sending message from one server to the inbox of another') @@ -171,7 +171,7 @@ def testPostMessageBetweenServers(): httpPrefix='http' useTor=False federationList=['127.0.0.50','127.0.0.100'] - capsList=[] + ocapGranted={} baseDir=os.getcwd() if os.path.isdir(baseDir+'/.tests'): @@ -182,12 +182,12 @@ def testPostMessageBetweenServers(): aliceDir=baseDir+'/.tests/alice' aliceDomain='127.0.0.50' alicePort=61935 - thrAlice = threadWithTrace(target=createServerAlice,args=(aliceDir,aliceDomain,alicePort,federationList,capsList,True,True),daemon=True) + thrAlice = threadWithTrace(target=createServerAlice,args=(aliceDir,aliceDomain,alicePort,federationList,ocapGranted,True,True),daemon=True) bobDir=baseDir+'/.tests/bob' bobDomain='127.0.0.100' bobPort=61936 - thrBob = threadWithTrace(target=createServerBob,args=(bobDir,bobDomain,bobPort,federationList,capsList,True,True),daemon=True) + thrBob = threadWithTrace(target=createServerBob,args=(bobDir,bobDomain,bobPort,federationList,ocapGranted,True,True),daemon=True) thrAlice.start() thrBob.start() @@ -214,7 +214,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, capsList, 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, ocapGranted, aliceSendThreads, alicePostLog, aliceCachedWebfingers,alicePersonCache,inReplyTo, inReplyToAtomUri, subject) print('sendResult: '+str(sendResult)) queuePath=bobDir+'/accounts/bob@'+bobDomain+'/queue' @@ -254,7 +254,7 @@ def testFollowBetweenServers(): httpPrefix='http' useTor=False federationList=['127.0.0.42','127.0.0.64'] - capsList=[] + ocapGranted={} baseDir=os.getcwd() if os.path.isdir(baseDir+'/.tests'): @@ -265,12 +265,12 @@ def testFollowBetweenServers(): aliceDir=baseDir+'/.tests/alice' aliceDomain='127.0.0.42' alicePort=61935 - thrAlice = threadWithTrace(target=createServerAlice,args=(aliceDir,aliceDomain,alicePort,federationList,capsList,False,False),daemon=True) + thrAlice = threadWithTrace(target=createServerAlice,args=(aliceDir,aliceDomain,alicePort,federationList,ocapGranted,False,False),daemon=True) bobDir=baseDir+'/.tests/bob' bobDomain='127.0.0.64' bobPort=61936 - thrBob = threadWithTrace(target=createServerBob,args=(bobDir,bobDomain,bobPort,federationList,capsList,False,False),daemon=True) + thrBob = threadWithTrace(target=createServerBob,args=(bobDir,bobDomain,bobPort,federationList,ocapGranted,False,False),daemon=True) thrAlice.start() thrBob.start() @@ -305,7 +305,7 @@ def testFollowBetweenServers(): sendFollowRequest(sessionAlice,aliceDir, \ 'alice',aliceDomain,alicePort,httpPrefix, \ 'bob',bobDomain,bobPort,httpPrefix, \ - clientToServer,federationList,capsList, + clientToServer,federationList,ocapGranted, aliceSendThreads,alicePostLog, \ aliceCachedWebfingers,alicePersonCache,True) print('sendResult: '+str(sendResult)) diff --git a/utils.py b/utils.py index 6506bafa0..0a048dde7 100644 --- a/utils.py +++ b/utils.py @@ -49,9 +49,9 @@ def domainPermitted(domain: str, federationList: []): return True return False -def urlPermitted(url: str, federationList: [],capsList: [],capability: str): - if capsList: - if not isCapable(url,capsList,capability): +def urlPermitted(url: str, federationList: [],ocapGranted: {},capability: str): + if ocapGranted: + if not isCapable(url,ocapGranted,capability): return False if len(federationList)==0: return True