Adding capabilities to posts

master
Bob Mottram 2019-07-06 11:33:57 +01:00
parent f3065516ae
commit c9d62e8361
12 changed files with 79 additions and 54 deletions

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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={}

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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)

View File

@ -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'

View File

@ -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: