diff --git a/acceptreject.py b/acceptreject.py index 69b32add2..9c9545585 100644 --- a/acceptreject.py +++ b/acceptreject.py @@ -12,13 +12,13 @@ from utils import getStatusNumber from utils import createOutboxDir from utils import urlPermitted -def createAcceptReject(baseDir: str,federationList: [],nickname: str,domain: str,port: int,toUrl: str,ccUrl: str,httpPrefix: str,objectUrl: str,acceptType: str) -> {}: +def createAcceptReject(baseDir: str,federationList: [],capsList: [],nickname: str,domain: str,port: int,toUrl: str,ccUrl: str,httpPrefix: str,objectUrl: str,acceptType: str) -> {}: """Accepts or rejects something (eg. a follow request) Typically toUrl will be https://www.w3.org/ns/activitystreams#Public and ccUrl might be a specific person favorited or repeated and the followers url objectUrl is typically the url of the message, corresponding to url or atomUri in createPostBase """ - if not urlPermitted(objectUrl,federationList): + if not urlPermitted(objectUrl,federationList,capsList,"inbox:write"): return None if port!=80 and port!=443: diff --git a/announce.py b/announce.py index a639173b2..0b587721f 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: [], \ +def createAnnounce(baseDir: str,federationList: [], capsList: [], \ nickname: str, domain: str, port: int, \ toUrl: str, ccUrl: str, httpPrefix: str, \ objectUrl: str, saveToFile: bool) -> {}: @@ -21,7 +21,7 @@ def createAnnounce(baseDir: str,federationList: [], \ and ccUrl might be a specific person favorited or repeated and the followers url objectUrl is typically the url of the message, corresponding to url or atomUri in createPostBase """ - if not urlPermitted(objectUrl,federationList): + if not urlPermitted(objectUrl,federationList,capsList,"inbox:write"): return None if port!=80 and port!=443: diff --git a/capabilities.py b/capabilities.py index 883a94f16..c843d286d 100644 --- a/capabilities.py +++ b/capabilities.py @@ -16,10 +16,7 @@ def sendCapabilitiesRequest(baseDir: str,httpPrefix: str,domain: str,requestedAc capRequest = { "id": httpPrefix+"://"+requestedDomain+"/caps/request/"+capId, "type": "Request", - "capability": { - "inbox": inbox, - "objects": objects - }, + "capability": ["inbox:write","objects:read"], "actor": requestedActor } #TODO @@ -30,10 +27,7 @@ def sendCapabilitiesAccept(baseDir: str,httpPrefix: str,nickname: str,domain: st capAccept = { "id": httpPrefix+"://"+domain+"/caps/"+capId, "type": "Capability", - "capability": { - "inbox": inbox, - "objects": objects - }, + "capability": ["inbox:write","objects:read"], "scope": acceptedActor, "actor": httpPrefix+"://"+domain } @@ -41,9 +35,10 @@ def sendCapabilitiesAccept(baseDir: str,httpPrefix: str,nickname: str,domain: st capAccept['actor']=httpPrefix+"://"+domain+'/users/'+nickname #TODO -def isCapable(actor: str,capsJson: []) -> bool: +def isCapable(actor: str,capsJson: [],capability: str) -> bool: # is the given actor capable of using the current resource? for cap in capsJson: if cap['scope'] in actor: - return True + if capability in cap['capability']: + return True return False diff --git a/daemon.py b/daemon.py index bc7249486..06f75da5d 100644 --- a/daemon.py +++ b/daemon.py @@ -375,7 +375,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy=False return - if not inboxPermittedMessage(self.server.domain,messageJson,self.server.federationList): + if not inboxPermittedMessage(self.server.domain,messageJson,self.server.federationList,self.server.capsList): if self.server.debug: # https://www.youtube.com/watch?v=K3PrSj9XEu4 print('DEBUG: Ah Ah Ah') @@ -421,7 +421,7 @@ class PubServer(BaseHTTPRequestHandler): self.end_headers() self.server.POSTbusy=False -def runDaemon(baseDir: str,domain: str,port=80,httpPrefix='https',fedList=[],useTor=False,debug=False) -> None: +def runDaemon(baseDir: str,domain: str,port=80,httpPrefix='https',fedList=[],capsList=[],useTor=False,debug=False) -> None: if len(domain)==0: domain='localhost' if '.' not in domain: @@ -436,6 +436,7 @@ def runDaemon(baseDir: str,domain: str,port=80,httpPrefix='https',fedList=[],use httpd.httpPrefix=httpPrefix httpd.debug=debug httpd.federationList=fedList.copy() + httpd.capsList=capsList.copy() httpd.baseDir=baseDir httpd.personCache={} httpd.cachedWebfingers={} diff --git a/epicyon.py b/epicyon.py index 2da7ba572..81b5f2858 100644 --- a/epicyon.py +++ b/epicyon.py @@ -286,7 +286,7 @@ if args.changepassword: if not args.domain and not domain: print('Specify a domain with --domain [name]') sys.exit() - + federationList=[] if args.federationList: if len(args.federationList)==1: @@ -304,6 +304,8 @@ else: if configFederationList: federationList=configFederationList +capsList=[] + if federationList: print('Federating with: '+str(federationList)) @@ -322,4 +324,4 @@ if not os.path.isdir(baseDir+'/accounts/capabilities@'+domain): print('Creating capabilities account which can sign requests') createCapabilitiesInbox(baseDir,'capabilities',domain,port,httpPrefix) -runDaemon(baseDir,domain,port,httpPrefix,federationList,useTor,debug) +runDaemon(baseDir,domain,port,httpPrefix,federationList,capsList,useTor,debug) diff --git a/follow.py b/follow.py index e03ca102b..8fbdd1adb 100644 --- a/follow.py +++ b/follow.py @@ -13,6 +13,7 @@ import sys from person import validNickname from utils import domainPermitted from posts import sendSignedJson +from capabilities import isCapable def getFollowersOfPerson(baseDir: str,nickname: str,domain: str,followFile='following.txt') -> []: """Returns a list containing the followers of the given person @@ -268,7 +269,7 @@ def receiveFollowRequest(session,baseDir: str,httpPrefix: str,port: int,sendThre def sendFollowRequest(baseDir: str,nickname: str,domain: str,port: int,httpPrefix: str, \ followNickname: str,followDomain: str,followPort: bool,followHttpPrefix: str, \ - federationList: []) -> {}: + federationList: [],capsList: []) -> {}: """Gets the json object for sending a follow request """ if not domainPermitted(followDomain,federationList): @@ -280,9 +281,16 @@ def sendFollowRequest(baseDir: str,nickname: str,domain: str,port: int,httpPrefi if followPort!=80 and followPort!=443: followDomain=followDomain+':'+str(followPort) + followActor=httpPrefix+'://'+domain+'/users/'+nickname + + # check that we are capable + if capsList: + if not isCapable(followActor,capsList,'inbox:write'): + return None + newFollowJson = { 'type': 'Follow', - 'actor': httpPrefix+'://'+domain+'/users/'+nickname, + 'actor': followActor, 'object': followHttpPrefix+'://'+followDomain+'/users/'+followNickname } diff --git a/inbox.py b/inbox.py index 8098297f3..b5ee5c997 100644 --- a/inbox.py +++ b/inbox.py @@ -59,7 +59,7 @@ def inboxMessageHasParams(messageJson: {}) -> bool: return False return True -def inboxPermittedMessage(domain: str,messageJson: {},federationList: []) -> bool: +def inboxPermittedMessage(domain: str,messageJson: {},federationList: [],capsList: []) -> bool: """ check that we are receiving from a permitted domain """ testParam='actor' @@ -70,13 +70,13 @@ def inboxPermittedMessage(domain: str,messageJson: {},federationList: []) -> boo if domain in actor: return True - if not urlPermitted(actor,federationList): + if not urlPermitted(actor,federationList,capsList,"inbox:write"): return False if messageJson.get('object'): if messageJson['object'].get('inReplyTo'): inReplyTo=messageJson['object']['inReplyTo'] - if not urlPermitted(inReplyTo, federationList): + if not urlPermitted(inReplyTo,federationList,capsList): return False return True diff --git a/like.py b/like.py index 044528d7b..2584e60ce 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): + if not urlPermitted(objectUrl,federationList,capsList,"inbox:write"): return None if port!=80 and port!=443: diff --git a/posts.py b/posts.py index 322cab541..28a7a9e51 100644 --- a/posts.py +++ b/posts.py @@ -30,6 +30,7 @@ from httpsig import createSignedHeader from utils import getStatusNumber from utils import createPersonDir from utils import urlPermitted +from capabilities import isCapable try: from BeautifulSoup import BeautifulSoup except ImportError: @@ -153,7 +154,8 @@ def getPersonBox(session,wfRequest: {},personCache: {},boxName='inbox') -> (str, return boxJson,pubKeyId,pubKey,personId,sharedInbox,capabilityAcquisition def getPosts(session,outboxUrl: str,maxPosts: int,maxMentions: int, \ - maxEmoji: int,maxAttachments: int,federationList: [], \ + maxEmoji: int,maxAttachments: int, \ + federationList: [], capsList: [],\ personCache: {},raw: bool,simple: bool) -> {}: personPosts={} if not outboxUrl: @@ -192,7 +194,7 @@ def getPosts(session,outboxUrl: str,maxPosts: int,maxMentions: int, \ if tagItem.get('name') and tagItem.get('icon'): if tagItem['icon'].get('url'): # No emoji from non-permitted domains - if urlPermitted(tagItem['icon']['url'],federationList): + if urlPermitted(tagItem['icon']['url'],federationList,capsList,"objects:read"): emojiName=tagItem['name'] emojiIcon=tagItem['icon']['url'] emoji[emojiName]=emojiIcon @@ -214,7 +216,7 @@ def getPosts(session,outboxUrl: str,maxPosts: int,maxMentions: int, \ if item['object'].get('inReplyTo'): if item['object']['inReplyTo']: # No replies to non-permitted domains - if not urlPermitted(item['object']['inReplyTo'],federationList): + if not urlPermitted(item['object']['inReplyTo'],federationList,capsList,"objects:read"): continue inReplyTo = item['object']['inReplyTo'] @@ -222,7 +224,7 @@ def getPosts(session,outboxUrl: str,maxPosts: int,maxMentions: int, \ if item['object'].get('conversation'): if item['object']['conversation']: # no conversations originated in non-permitted domains - if urlPermitted(item['object']['conversation'],federationList): + if urlPermitted(item['object']['conversation'],federationList,"objects:read"): conversation = item['object']['conversation'] attachment = [] @@ -231,7 +233,7 @@ def getPosts(session,outboxUrl: str,maxPosts: int,maxMentions: int, \ for attach in item['object']['attachment']: if attach.get('name') and attach.get('url'): # no attachments from non-permitted domains - if urlPermitted(attach['url'],federationList): + if urlPermitted(attach['url'],federationList,capsList,"objects:read"): attachment.append([attach['name'],attach['url']]) sensitive = False @@ -308,6 +310,7 @@ def savePostToBox(baseDir: str,httpPrefix: str,postId: str,nickname: str, domain 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: [], \ inReplyTo=None, inReplyToAtomUri=None, subject=None) -> {}: """Creates a message """ @@ -331,11 +334,16 @@ def createPostBase(baseDir: str,nickname: str, domain: str, port: int, \ summary=subject sensitive=True if not clientToServer: + actorUrl=httpPrefix+'://'+domain+'/users/'+nickname + if capsList: + if not isCapable(actorUrl,capsList,'inbox:write'): + return None + newPost = { 'id': newPostId+'/activity', 'capability': capabilityUrl, 'type': 'Create', - 'actor': httpPrefix+'://'+domain+'/users/'+nickname, + 'actor': actorUrl, 'published': published, 'to': [toUrl], 'cc': [], @@ -433,7 +441,7 @@ def outboxMessageCreateWrap(httpPrefix: str,nickname: str,domain: str,messageJso def createPublicPost(baseDir: str, nickname: str, domain: str, port: int,httpPrefix: str, \ content: str, followersOnly: bool, saveToFile: bool, - clientToServer: bool, \ + clientToServer: bool, capsList: [],\ inReplyTo=None, inReplyToAtomUri=None, subject=None) -> {}: """Public post to the outbox """ @@ -441,17 +449,18 @@ def createPublicPost(baseDir: str, 'https://www.w3.org/ns/activitystreams#Public', \ httpPrefix+'://'+domain+'/users/'+nickname+'/followers', \ httpPrefix, content, followersOnly, saveToFile, clientToServer, \ + capsList, inReplyTo, inReplyToAtomUri, subject) -def threadSendPost(session,postJsonObject: {},federationList: [],inboxUrl: str, \ - baseDir: str,signatureHeaderJson: {},postLog: []) -> None: +def threadSendPost(session,postJsonObject: {},federationList: [],capsList: [],\ + inboxUrl: str, baseDir: str,signatureHeaderJson: {},postLog: []) -> None: """Sends a post with exponential backoff """ tries=0 backoffTime=60 for attempt in range(20): postResult = postJson(session,postJsonObject,federationList, \ - inboxUrl,signatureHeaderJson) + capsList,inboxUrl,signatureHeaderJson) if postResult: postLog.append(postJsonObject['published']+' '+postResult+'\n') # keep the length of the log finite @@ -471,7 +480,8 @@ def threadSendPost(session,postJsonObject: {},federationList: [],inboxUrl: str, 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: [], \ + saveToFile: bool, clientToServer: bool, \ + federationList: [], capsList: [],\ sendThreads: [], postLog: [], cachedWebfingers: {},personCache: {}, \ inReplyTo=None, inReplyToAtomUri=None, subject=None) -> int: """Post to another inbox @@ -519,8 +529,8 @@ def sendPost(session,baseDir: str,nickname: str, domain: str, port: int, \ createPostBase(baseDir,nickname,domain,port, \ toPersonId,cc,httpPrefix,content, \ followersOnly,saveToFile,clientToServer, \ - inReplyTo,inReplyToAtomUri, \ - subject) + capsList, \ + inReplyTo,inReplyToAtomUri,subject) # get the senders private key privateKeyPem=getPersonKey(nickname,domain,baseDir,'private') @@ -543,6 +553,7 @@ def sendPost(session,baseDir: str,nickname: str, domain: str, port: int, \ thr = threadWithTrace(target=threadSendPost,args=(session, \ postJsonObject.copy(), \ federationList, \ + capsList, \ inboxUrl,baseDir, \ signatureHeaderJson.copy(), \ postLog),daemon=True) @@ -552,7 +563,8 @@ def sendPost(session,baseDir: str,nickname: str, domain: str, port: int, \ 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: [], \ + httpPrefix: str, saveToFile: bool, clientToServer: bool, \ + federationList: [], capsList: [], \ sendThreads: [], postLog: [], cachedWebfingers: {},personCache: {}) -> int: """Sends a signed json object to an inbox/outbox """ @@ -616,6 +628,7 @@ def sendSignedJson(postJsonObject: {},session,baseDir: str,nickname: str, domain thr = threadWithTrace(target=threadSendPost,args=(session, \ postJsonObject.copy(), \ federationList, \ + capsList, \ inboxUrl,baseDir, \ signatureHeaderJson.copy(), \ postLog),daemon=True) diff --git a/session.py b/session.py index c9d4007a7..201903592 100644 --- a/session.py +++ b/session.py @@ -39,11 +39,12 @@ def getJson(session,url: str,headers: {},params: {}) -> {}: pass return None -def postJson(session,postJsonObject: {},federationList: [],inboxUrl: str,headers: {}) -> str: +def postJson(session,postJsonObject: {},federationList: [],capsList: [],inboxUrl: str,headers: {}) -> str: """Post a json message to the inbox of another person """ + # check that we are posting to a permitted domain - if not urlPermitted(inboxUrl,federationList): + if not urlPermitted(inboxUrl,federationList,capsList,"inbox:write"): return None postResult = session.post(url = inboxUrl, data = json.dumps(postJsonObject), headers=headers) diff --git a/tests.py b/tests.py index c67540a38..b37265c07 100644 --- a/tests.py +++ b/tests.py @@ -107,7 +107,7 @@ def testThreads(): thr.join() assert thr.isAlive()==False -def createServerAlice(path: str,domain: str,port: int,federationList: []): +def createServerAlice(path: str,domain: str,port: int,federationList: [],capsList: []): print('Creating test server: Alice on port '+str(port)) if os.path.isdir(path): shutil.rmtree(path) @@ -123,15 +123,15 @@ def createServerAlice(path: str,domain: str,port: int,federationList: []): deleteAllPosts(path,nickname,domain,'outbox') followPerson(path,nickname,domain,'bob','127.0.0.100:61936',federationList) followerOfPerson(path,nickname,domain,'bob','127.0.0.100:61936',federationList) - 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) + 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) global testServerAliceRunning testServerAliceRunning = True print('Server running: Alice') - runDaemon(path,domain,port,httpPrefix,federationList,useTor,True) + runDaemon(path,domain,port,httpPrefix,federationList,capsList,useTor,True) -def createServerBob(path: str,domain: str,port: int,federationList: []): +def createServerBob(path: str,domain: str,port: int,federationList: [],capsList: []): print('Creating test server: Bob on port '+str(port)) if os.path.isdir(path): shutil.rmtree(path) @@ -147,13 +147,13 @@ def createServerBob(path: str,domain: str,port: int,federationList: []): deleteAllPosts(path,nickname,domain,'outbox') followPerson(path,nickname,domain,'alice','127.0.0.50:61935',federationList) followerOfPerson(path,nickname,domain,'alice','127.0.0.50:61935',federationList) - 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) + 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) global testServerBobRunning testServerBobRunning = True print('Server running: Bob') - runDaemon(path,domain,port,httpPrefix,federationList,useTor,True) + runDaemon(path,domain,port,httpPrefix,federationList,capsList,useTor,True) def testPostMessageBetweenServers(): print('Testing sending message from one server to the inbox of another') @@ -166,6 +166,7 @@ def testPostMessageBetweenServers(): httpPrefix='http' useTor=False federationList=['127.0.0.50','127.0.0.100'] + capsList=[] baseDir=os.getcwd() if not os.path.isdir(baseDir+'/.tests'): @@ -175,12 +176,12 @@ def testPostMessageBetweenServers(): aliceDir=baseDir+'/.tests/alice' aliceDomain='127.0.0.50' alicePort=61935 - thrAlice = threadWithTrace(target=createServerAlice,args=(aliceDir,aliceDomain,alicePort,federationList),daemon=True) + thrAlice = threadWithTrace(target=createServerAlice,args=(aliceDir,aliceDomain,alicePort,federationList,capsList),daemon=True) bobDir=baseDir+'/.tests/bob' bobDomain='127.0.0.100' bobPort=61936 - thrBob = threadWithTrace(target=createServerBob,args=(bobDir,bobDomain,bobPort,federationList),daemon=True) + thrBob = threadWithTrace(target=createServerBob,args=(bobDir,bobDomain,bobPort,federationList,capsList),daemon=True) thrAlice.start() thrBob.start() @@ -207,7 +208,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, 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, capsList, aliceSendThreads, alicePostLog, aliceCachedWebfingers,alicePersonCache,inReplyTo, inReplyToAtomUri, subject) print('sendResult: '+str(sendResult)) queuePath=bobDir+'/accounts/bob@'+bobDomain+'/queue' diff --git a/utils.py b/utils.py index dd2e28eab..9688344ac 100644 --- a/utils.py +++ b/utils.py @@ -8,6 +8,7 @@ __status__ = "Production" import os import datetime +from capabilities import isCapable def getStatusNumber() -> (str,str): """Returns the status number and published date @@ -48,7 +49,10 @@ def domainPermitted(domain: str, federationList: []): return True return False -def urlPermitted(url: str, federationList: []): +def urlPermitted(url: str, federationList: [],capsList: [],capability: str): + if capsList: + if not isCapable(url,capsList,capability): + return False if len(federationList)==0: return True for domain in federationList: