forked from indymedia/epicyon
Capability on post is a list
parent
8a1fd82584
commit
db68b34cc5
|
@ -102,7 +102,7 @@ Follow Accept from **Bob** to **Alice** with attached capabilities.
|
||||||
'type': 'Accept'}
|
'type': 'Accept'}
|
||||||
```
|
```
|
||||||
|
|
||||||
When posts are subsequently sent from the following instance (server-to-server) they should have the corresponding capability id string attached within the Create wrapper. In the above example that would be **http://bobdomain.net/caps/rOYtHApyr4ZWDUgEE1KqjhTe0kI3T2wJ**. It should contain a random string which is hard to guess by brute force methods.
|
When posts are subsequently sent from the following instance (server-to-server) they should have the corresponding capability id string attached within the Create wrapper. To handle the *shared inbox* scenario this should be a list rather than a single string. In the above example that would be *['http://bobdomain.net/caps/rOYtHApyr4ZWDUgEE1KqjhTe0kI3T2wJ']*. It should contain a random string which is hard to guess by brute force methods.
|
||||||
|
|
||||||
``` text
|
``` text
|
||||||
Alice
|
Alice
|
||||||
|
@ -141,6 +141,12 @@ Subsequently **Bob** could change the stored capabilities for **Alice** in their
|
||||||
|
|
||||||
Object capabilities can be strictly enforced by adding the **--ocap** option when running the server. The only activities which it is not enforced upon are **Follow** and **Accept**. Anyone can create a follow request or accept updated capabilities.
|
Object capabilities can be strictly enforced by adding the **--ocap** option when running the server. The only activities which it is not enforced upon are **Follow** and **Accept**. Anyone can create a follow request or accept updated capabilities.
|
||||||
|
|
||||||
|
## Object capabilities in the shared inbox scenario
|
||||||
|
|
||||||
|
Shared inboxes are obviously essential for any kind of scalability, otherwise there would be vast amounts of duplicated messages being dumped onto the intertubes like a big truck.
|
||||||
|
|
||||||
|
With the shared inbox instead of sending from Alice to 500 of her fans on a different instance - repeatedly sending the same message to individual inboxes - a single message is sent to its shared inbox (which has its own special account called 'inbox') and it then decides how to distribute that. If a list of capability ids is attached to the message which gets sent to the shared inbox then the receiving server can use that.
|
||||||
|
|
||||||
## Some capabilities
|
## Some capabilities
|
||||||
|
|
||||||
*inbox:write* - follower can post anything to your inbox
|
*inbox:write* - follower can post anything to your inbox
|
||||||
|
|
|
@ -491,7 +491,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
else:
|
else:
|
||||||
if self.path == '/sharedInbox' or self.path == '/inbox':
|
if self.path == '/sharedInbox' or self.path == '/inbox':
|
||||||
print('DEBUG: POST to shared inbox')
|
print('DEBUG: POST to shared inbox')
|
||||||
if self._updateInboxQueue('sharedinbox',messageJson):
|
if self._updateInboxQueue('inbox',messageJson):
|
||||||
return
|
return
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
|
@ -332,9 +332,9 @@ if not os.path.isdir(baseDir+'/accounts/'+nickname+'@'+domain):
|
||||||
setConfigParam(baseDir,'adminPassword',adminPassword)
|
setConfigParam(baseDir,'adminPassword',adminPassword)
|
||||||
createPerson(baseDir,nickname,domain,port,httpPrefix,True,adminPassword)
|
createPerson(baseDir,nickname,domain,port,httpPrefix,True,adminPassword)
|
||||||
|
|
||||||
if not os.path.isdir(baseDir+'/accounts/sharedinbox@'+domain):
|
if not os.path.isdir(baseDir+'/accounts/inbox@'+domain):
|
||||||
print('Creating shared inbox')
|
print('Creating shared inbox: inbox@'+domain)
|
||||||
createSharedInbox(baseDir,'sharedinbox',domain,port,httpPrefix)
|
createSharedInbox(baseDir,'inbox',domain,port,httpPrefix)
|
||||||
|
|
||||||
if not os.path.isdir(baseDir+'/accounts/capabilities@'+domain):
|
if not os.path.isdir(baseDir+'/accounts/capabilities@'+domain):
|
||||||
print('Creating capabilities account which can sign requests')
|
print('Creating capabilities account which can sign requests')
|
||||||
|
|
|
@ -33,7 +33,7 @@ def getFollowersOfPerson(baseDir: str, \
|
||||||
for subdir, dirs, files in os.walk(baseDir+'/accounts'):
|
for subdir, dirs, files in os.walk(baseDir+'/accounts'):
|
||||||
for account in dirs:
|
for account in dirs:
|
||||||
filename = os.path.join(subdir, account)+'/'+followFile
|
filename = os.path.join(subdir, account)+'/'+followFile
|
||||||
if account == handle or account.startswith('sharedinbox@'):
|
if account == handle or account.startswith('inbox@'):
|
||||||
continue
|
continue
|
||||||
if not os.path.isfile(filename):
|
if not os.path.isfile(filename):
|
||||||
continue
|
continue
|
||||||
|
|
100
inbox.py
100
inbox.py
|
@ -124,7 +124,7 @@ def savePostToInboxQueue(baseDir: str,httpPrefix: str,nickname: str, domain: str
|
||||||
filename=inboxQueueDir+'/'+postId.replace('/','#')+'.json'
|
filename=inboxQueueDir+'/'+postId.replace('/','#')+'.json'
|
||||||
|
|
||||||
sharedInboxItem=False
|
sharedInboxItem=False
|
||||||
if nickname=='sharedinbox':
|
if nickname=='inbox':
|
||||||
sharedInboxItem=True
|
sharedInboxItem=True
|
||||||
|
|
||||||
newQueueItem = {
|
newQueueItem = {
|
||||||
|
@ -179,56 +179,70 @@ def runInboxQueue(baseDir: str,httpPrefix: str,sendThreads: [],postLog: [],cache
|
||||||
with open(queueFilename, 'r') as fp:
|
with open(queueFilename, 'r') as fp:
|
||||||
queueJson=commentjson.load(fp)
|
queueJson=commentjson.load(fp)
|
||||||
|
|
||||||
# check that capabilities are accepted
|
sentToSharedInbox=False
|
||||||
capabilitiesPassed=False
|
if queueJson['post'].get('actor'):
|
||||||
if queueJson['post'].get('capability'):
|
if queueJson['post']['actor'].endswith('/inbox'):
|
||||||
if isinstance(queueJson['post']['capability'], dict):
|
sentToSharedInbox=True
|
||||||
if debug:
|
|
||||||
print('DEBUG: capability is a dictionary when it should be a string')
|
if sentToSharedInbox:
|
||||||
os.remove(queueFilename)
|
# if this is arriving at the shared inbox then
|
||||||
queue.pop(0)
|
# don't do the capabilities checks
|
||||||
continue
|
capabilitiesPassed=True
|
||||||
ocapFilename= \
|
# TODO how to handle capabilities in the shared inbox scenario?
|
||||||
getOcapFilename(baseDir, \
|
# should 'capability' be a list instead of a single value?
|
||||||
queueJson['nickname'],queueJson['domain'], \
|
else:
|
||||||
queueJson['post']['actor'],'accept')
|
# check that capabilities are accepted
|
||||||
if not os.path.isfile(ocapFilename):
|
capabilitiesPassed=False
|
||||||
if debug:
|
if queueJson['post'].get('capability'):
|
||||||
print('DEBUG: capabilities for '+ \
|
if not isinstance(queueJson['post']['capability'], list):
|
||||||
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:
|
if debug:
|
||||||
print('DEBUG: capabilities for '+queueJson['post']['actor']+' do not contain an id')
|
print('DEBUG: capability on post should be a list')
|
||||||
os.remove(queueFilename)
|
os.remove(queueFilename)
|
||||||
queue.pop(0)
|
queue.pop(0)
|
||||||
continue
|
continue
|
||||||
if oc['id']!=queueJson['post']['capability']:
|
capabilityIdList=queueJson['post']['capability']
|
||||||
|
|
||||||
|
ocapFilename= \
|
||||||
|
getOcapFilename(baseDir, \
|
||||||
|
queueJson['nickname'],queueJson['domain'], \
|
||||||
|
queueJson['post']['actor'],'accept')
|
||||||
|
if not os.path.isfile(ocapFilename):
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: capability id mismatch')
|
print('DEBUG: capabilities for '+ \
|
||||||
|
queueJson['post']['actor']+' do not exist')
|
||||||
os.remove(queueFilename)
|
os.remove(queueFilename)
|
||||||
queue.pop(0)
|
queue.pop(0)
|
||||||
continue
|
continue
|
||||||
if not oc.get('capability'):
|
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']!=capabilityIdList[0]:
|
||||||
|
if debug:
|
||||||
|
print('DEBUG: capability id mismatch')
|
||||||
|
os.remove(queueFilename)
|
||||||
|
queue.pop(0)
|
||||||
|
continue
|
||||||
|
if not oc.get('capability'):
|
||||||
|
if debug:
|
||||||
|
print('DEBUG: missing capability list')
|
||||||
|
os.remove(queueFilename)
|
||||||
|
queue.pop(0)
|
||||||
|
continue
|
||||||
|
if not CapablePost(queueJson['post'],oc['capability'],debug):
|
||||||
|
if debug:
|
||||||
|
print('DEBUG: insufficient capabilities to write to inbox from '+ \
|
||||||
|
queueJson['post']['actor'])
|
||||||
|
os.remove(queueFilename)
|
||||||
|
queue.pop(0)
|
||||||
|
continue
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: missing capability list')
|
print('DEBUG: object capabilities check success')
|
||||||
os.remove(queueFilename)
|
capabilitiesPassed=True
|
||||||
queue.pop(0)
|
|
||||||
continue
|
|
||||||
if not CapablePost(queueJson['post'],oc['capability'],debug):
|
|
||||||
if debug:
|
|
||||||
print('DEBUG: insufficient capabilities to write to inbox from '+ \
|
|
||||||
queueJson['post']['actor'])
|
|
||||||
os.remove(queueFilename)
|
|
||||||
queue.pop(0)
|
|
||||||
continue
|
|
||||||
if debug:
|
|
||||||
print('DEBUG: object capabilities check success')
|
|
||||||
capabilitiesPassed=True
|
|
||||||
|
|
||||||
if ocapAlways and not capabilitiesPassed:
|
if ocapAlways and not capabilitiesPassed:
|
||||||
# Allow follow types through
|
# Allow follow types through
|
||||||
|
@ -347,7 +361,7 @@ def runInboxQueue(baseDir: str,httpPrefix: str,sendThreads: [],postLog: [],cache
|
||||||
# post already exists in this person's inbox
|
# post already exists in this person's inbox
|
||||||
continue
|
continue
|
||||||
# We could do this in a more storage space efficient way
|
# We could do this in a more storage space efficient way
|
||||||
# by linking to the inbox of sharedinbox@domain
|
# by linking to the inbox of inbox@domain
|
||||||
# However, this allows for easy deletion by individuals
|
# However, this allows for easy deletion by individuals
|
||||||
# without affecting any other people
|
# without affecting any other people
|
||||||
copyfile(queueFilename, destination)
|
copyfile(queueFilename, destination)
|
||||||
|
|
|
@ -140,7 +140,7 @@ def validNickname(nickname: str) -> bool:
|
||||||
for c in forbiddenChars:
|
for c in forbiddenChars:
|
||||||
if c in nickname:
|
if c in nickname:
|
||||||
return False
|
return False
|
||||||
reservedNames=['inbox','outbox','following','followers','sharedinbox','capabilities']
|
reservedNames=['inbox','outbox','following','followers','capabilities']
|
||||||
if nickname in reservedNames:
|
if nickname in reservedNames:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
84
posts.py
84
posts.py
|
@ -351,19 +351,17 @@ def createPostBase(baseDir: str,nickname: str, domain: str, port: int, \
|
||||||
# if capabilities have been granted for this actor
|
# if capabilities have been granted for this actor
|
||||||
# then get the corresponding id
|
# then get the corresponding id
|
||||||
capabilityId=None
|
capabilityId=None
|
||||||
ocapFilename= getOcapFilename(baseDir,nickname,domain,toUrl,'granted')
|
capabilityIdList=[]
|
||||||
#print('ocapFilename: '+ocapFilename)
|
ocapFilename=getOcapFilename(baseDir,nickname,domain,toUrl,'granted')
|
||||||
if os.path.isfile(ocapFilename):
|
if os.path.isfile(ocapFilename):
|
||||||
with open(ocapFilename, 'r') as fp:
|
with open(ocapFilename, 'r') as fp:
|
||||||
oc=commentjson.load(fp)
|
oc=commentjson.load(fp)
|
||||||
if oc.get('id'):
|
if oc.get('id'):
|
||||||
capabilityId=oc['id']
|
capabilityIdList=[oc['id']]
|
||||||
#else:
|
|
||||||
# print('ocapFilename: '+ocapFilename+' not found')
|
|
||||||
|
|
||||||
newPost = {
|
newPost = {
|
||||||
'id': newPostId+'/activity',
|
'id': newPostId+'/activity',
|
||||||
'capability': capabilityId,
|
'capability': capabilityIdList,
|
||||||
'type': 'Create',
|
'type': 'Create',
|
||||||
'actor': actorUrl,
|
'actor': actorUrl,
|
||||||
'published': published,
|
'published': published,
|
||||||
|
@ -464,6 +462,32 @@ def outboxMessageCreateWrap(httpPrefix: str,nickname: str,domain: str, \
|
||||||
httpPrefix+'://'+domain+'/users/'+nickname+'/statuses/'+statusNumber
|
httpPrefix+'://'+domain+'/users/'+nickname+'/statuses/'+statusNumber
|
||||||
return newPost
|
return newPost
|
||||||
|
|
||||||
|
def postIsAddressedToFollowers(baseDir: str,
|
||||||
|
nickname: str, domain: str, port: int,httpPrefix: str,
|
||||||
|
postJson: {}) -> bool:
|
||||||
|
"""Returns true if the given post is addressed to followers of the nickname
|
||||||
|
"""
|
||||||
|
if port!=80 and port!=443:
|
||||||
|
domain=domain+':'+str(port)
|
||||||
|
|
||||||
|
if not postJson.get('object'):
|
||||||
|
return False
|
||||||
|
if not postJson['object'].get('to'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
followersUrl=httpPrefix+'://'+domain+'/users/'+nickname+'/followers'
|
||||||
|
|
||||||
|
# does the followers url exist in 'to' or 'cc' lists?
|
||||||
|
addressedToFollowers=False
|
||||||
|
if followersUrl in postJson['object']['to']:
|
||||||
|
addressedToFollowers=True
|
||||||
|
if not addressedToFollowers:
|
||||||
|
if not postJson['object'].get('cc'):
|
||||||
|
return False
|
||||||
|
if followersUrl in postJson['object']['cc']:
|
||||||
|
addressedToFollowers=True
|
||||||
|
return addressedToFollowers
|
||||||
|
|
||||||
def createPublicPost(baseDir: str,
|
def createPublicPost(baseDir: str,
|
||||||
nickname: str, domain: str, port: int,httpPrefix: str, \
|
nickname: str, domain: str, port: int,httpPrefix: str, \
|
||||||
content: str, followersOnly: bool, saveToFile: bool,
|
content: str, followersOnly: bool, saveToFile: bool,
|
||||||
|
@ -626,6 +650,10 @@ def sendSignedJson(postJsonObject: {},session,baseDir: str, \
|
||||||
"""
|
"""
|
||||||
withDigest=True
|
withDigest=True
|
||||||
|
|
||||||
|
sharedInbox=False
|
||||||
|
if toNickname=='inbox':
|
||||||
|
sharedInbox=True
|
||||||
|
|
||||||
if toPort!=80 and toPort!=443:
|
if toPort!=80 and toPort!=443:
|
||||||
if ':' not in toDomain:
|
if ':' not in toDomain:
|
||||||
toDomain=toDomain+':'+str(toPort)
|
toDomain=toDomain+':'+str(toPort)
|
||||||
|
@ -633,7 +661,7 @@ def sendSignedJson(postJsonObject: {},session,baseDir: str, \
|
||||||
handle=httpPrefix+'://'+toDomain+'/@'+toNickname
|
handle=httpPrefix+'://'+toDomain+'/@'+toNickname
|
||||||
|
|
||||||
# lookup the inbox for the To handle
|
# lookup the inbox for the To handle
|
||||||
wfRequest = webfingerHandle(session,handle,httpPrefix,cachedWebfingers)
|
wfRequest=webfingerHandle(session,handle,httpPrefix,cachedWebfingers)
|
||||||
if not wfRequest:
|
if not wfRequest:
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: webfinger for '+handle+' failed')
|
print('DEBUG: webfinger for '+handle+' failed')
|
||||||
|
@ -645,18 +673,16 @@ def sendSignedJson(postJsonObject: {},session,baseDir: str, \
|
||||||
postToBox='outbox'
|
postToBox='outbox'
|
||||||
|
|
||||||
# get the actor inbox/outbox/capabilities for the To handle
|
# get the actor inbox/outbox/capabilities for the To handle
|
||||||
inboxUrl,pubKeyId,pubKey,toPersonId,sharedInbox,capabilityAcquisition = \
|
inboxUrl,pubKeyId,pubKey,toPersonId,sharedInboxUrl,capabilityAcquisition = \
|
||||||
getPersonBox(session,wfRequest,personCache,postToBox)
|
getPersonBox(session,wfRequest,personCache,postToBox)
|
||||||
|
|
||||||
# If there are more than one followers on the target domain
|
|
||||||
# then send to teh shared inbox indead of the individual inbox
|
|
||||||
if nickname=='capabilities':
|
if nickname=='capabilities':
|
||||||
inboxUrl=capabilityAcquisition
|
inboxUrl=capabilityAcquisition
|
||||||
if not capabilityAcquisition:
|
if not capabilityAcquisition:
|
||||||
return 2
|
return 2
|
||||||
else:
|
else:
|
||||||
if noOfFollowersOnDomain(baseDir,handle,toDomain)>1 and sharedInbox:
|
if sharedInbox and sharedInboxUrl:
|
||||||
inboxUrl=sharedInbox
|
inboxUrl=sharedInboxUrl
|
||||||
|
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: Sending to endpoint '+inboxUrl)
|
print('DEBUG: Sending to endpoint '+inboxUrl)
|
||||||
|
@ -702,6 +728,40 @@ def sendSignedJson(postJsonObject: {},session,baseDir: str, \
|
||||||
thr.start()
|
thr.start()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
def sendToFollowers(session,baseDir: str,
|
||||||
|
nickname: str, domain: str, port: int,httpPrefix: str,
|
||||||
|
postJsonObject: {}):
|
||||||
|
"""sends a post to the followers of the given nickname
|
||||||
|
"""
|
||||||
|
if not postIsAddressedToFollowers(baseDir,nickname,domain, \
|
||||||
|
port,httpPrefix,postJsonObject):
|
||||||
|
return
|
||||||
|
|
||||||
|
grouped=groupFollowersByDomain(baseDir,nickname,domain)
|
||||||
|
if not grouped:
|
||||||
|
return
|
||||||
|
|
||||||
|
# for each instance
|
||||||
|
for followerDomain,followerHandles in grouped.items():
|
||||||
|
toPort=port
|
||||||
|
index=0
|
||||||
|
toDomain=followerHandles[index].split('@')[1]
|
||||||
|
if ':' in toDomain:
|
||||||
|
toPort=toDomain.split(':')[1]
|
||||||
|
toDomain=toDomain.split(':')[0]
|
||||||
|
toNickname=followerHandles[index].split('@')[0]
|
||||||
|
cc=''
|
||||||
|
if len(followerHandles)>1:
|
||||||
|
nickname='inbox'
|
||||||
|
toNickname='inbox'
|
||||||
|
sendSignedJson(postJsonObject,session,baseDir, \
|
||||||
|
nickname,domain,port, \
|
||||||
|
toNickname,toDomain,toPort, \
|
||||||
|
cc,httpPrefix,True,clientToServer, \
|
||||||
|
federationList,ocapGranted, \
|
||||||
|
sendThreads,postLog,cachedWebfingers, \
|
||||||
|
personCache,debug)
|
||||||
|
|
||||||
def createInbox(baseDir: str,nickname: str,domain: str,port: int,httpPrefix: str, \
|
def createInbox(baseDir: str,nickname: str,domain: str,port: int,httpPrefix: str, \
|
||||||
itemsPerPage: int,headerOnly: bool,pageNumber=None) -> {}:
|
itemsPerPage: int,headerOnly: bool,pageNumber=None) -> {}:
|
||||||
return createBoxBase(baseDir,'inbox',nickname,domain,port,httpPrefix, \
|
return createBoxBase(baseDir,'inbox',nickname,domain,port,httpPrefix, \
|
||||||
|
|
Loading…
Reference in New Issue