master
Bob Mottram 2019-07-02 21:54:22 +01:00
parent 8276f24468
commit 306f9edf46
10 changed files with 135 additions and 86 deletions

View File

@ -12,7 +12,10 @@ from utils import getStatusNumber
from utils import createOutboxDir from utils import createOutboxDir
from utils import urlPermitted from utils import urlPermitted
def createAnnounce(baseDir: str,federationList: [],username: str, domain: str, port: int,toUrl: str, ccUrl: str, https: bool, objectUrl: str, saveToFile: bool) -> {}: def createAnnounce(baseDir: str,federationList: [], \
username: str, domain: str, port: int, \
toUrl: str, ccUrl: str, https: bool, \
objectUrl: str, saveToFile: bool) -> {}:
"""Creates an announce message """Creates an announce message
Typically toUrl will be https://www.w3.org/ns/activitystreams#Public 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 and ccUrl might be a specific person favorited or repeated and the followers url
@ -52,7 +55,9 @@ def createAnnounce(baseDir: str,federationList: [],username: str, domain: str, p
commentjson.dump(newAnnounce, fp, indent=4, sort_keys=False) commentjson.dump(newAnnounce, fp, indent=4, sort_keys=False)
return newAnnounce return newAnnounce
def announcePublic(baseDir: str,federationList: [],username: str, domain: str, port: int, https: bool, objectUrl: str, saveToFile: bool) -> {}: def announcePublic(baseDir: str,federationList: [], \
username: str, domain: str, port: int, https: bool, \
objectUrl: str, saveToFile: bool) -> {}:
"""Makes a public announcement """Makes a public announcement
""" """
prefix='https' prefix='https'
@ -65,9 +70,14 @@ def announcePublic(baseDir: str,federationList: [],username: str, domain: str, p
toUrl = 'https://www.w3.org/ns/activitystreams#Public' toUrl = 'https://www.w3.org/ns/activitystreams#Public'
ccUrl = prefix + '://'+fromDomain+'/users/'+username+'/followers' ccUrl = prefix + '://'+fromDomain+'/users/'+username+'/followers'
return createAnnounce(baseDir,username, domain, port,toUrl, ccUrl, https, objectUrl, saveToFile) return createAnnounce(baseDir,username, domain, port, \
toUrl, ccUrl, https, objectUrl, saveToFile)
def repeatPost(baseDir: str,federationList: [],username: str, domain: str, port: int, https: bool, announceUsername: str, announceDomain: str, announcePort: int, announceStatusNumber: int, announceHttps: bool, saveToFile: bool) -> {}: def repeatPost(baseDir: str,federationList: [], \
username: str, domain: str, port: int, https: bool, \
announceUsername: str, announceDomain: str, \
announcePort: int, announceHttps: bool, \
announceStatusNumber: int, saveToFile: bool) -> {}:
"""Repeats a given status post """Repeats a given status post
""" """
prefix='https' prefix='https'
@ -78,7 +88,8 @@ def repeatPost(baseDir: str,federationList: [],username: str, domain: str, port:
if announcePort!=80 and announcePort!=443: if announcePort!=80 and announcePort!=443:
announcedDomain=announcedDomain+':'+str(announcePort) announcedDomain=announcedDomain+':'+str(announcePort)
objectUrl = prefix + '://'+announcedDomain+'/users/'+announceUsername+'/statuses/'+str(announceStatusNumber) objectUrl = prefix + '://'+announcedDomain+'/users/'+ \
announceUsername+'/statuses/'+str(announceStatusNumber)
return announcePublic(baseDir,username, domain, port, https, objectUrl, saveToFile) return announcePublic(baseDir,username, domain, port, https, objectUrl, saveToFile)

View File

@ -116,32 +116,40 @@ class PubServer(BaseHTTPRequestHandler):
return return
print('############### _webfinger end') print('############### _webfinger end')
# get outbox feed for a person # get outbox feed for a person
outboxFeed=personOutboxJson(self.server.baseDir,self.server.domain,self.server.port,self.path,self.server.https,maxPostsInFeed) outboxFeed=personOutboxJson(self.server.baseDir,self.server.domain, \
self.server.port,self.path, \
self.server.https,maxPostsInFeed)
if outboxFeed: if outboxFeed:
self._set_headers('application/json') self._set_headers('application/json')
self.wfile.write(json.dumps(outboxFeed).encode('utf-8')) self.wfile.write(json.dumps(outboxFeed).encode('utf-8'))
self.server.GETbusy=False self.server.GETbusy=False
return return
following=getFollowingFeed(self.server.baseDir,self.server.domain,self.server.port,self.path,self.server.https,followsPerPage) following=getFollowingFeed(self.server.baseDir,self.server.domain, \
self.server.port,self.path, \
self.server.https,followsPerPage)
if following: if following:
self._set_headers('application/json') self._set_headers('application/json')
self.wfile.write(json.dumps(following).encode('utf-8')) self.wfile.write(json.dumps(following).encode('utf-8'))
self.server.GETbusy=False self.server.GETbusy=False
return return
followers=getFollowingFeed(self.server.baseDir,self.server.domain,self.server.port,self.path,self.server.https,followsPerPage,'followers') followers=getFollowingFeed(self.server.baseDir,self.server.domain, \
self.server.port,self.path, \
self.server.https,followsPerPage,'followers')
if followers: if followers:
self._set_headers('application/json') self._set_headers('application/json')
self.wfile.write(json.dumps(followers).encode('utf-8')) self.wfile.write(json.dumps(followers).encode('utf-8'))
self.server.GETbusy=False self.server.GETbusy=False
return return
# look up a person # look up a person
getPerson = personLookup(self.server.domain,self.path,self.server.baseDir) getPerson = personLookup(self.server.domain,self.path, \
self.server.baseDir)
if getPerson: if getPerson:
self._set_headers('application/json') self._set_headers('application/json')
self.wfile.write(json.dumps(getPerson).encode('utf-8')) self.wfile.write(json.dumps(getPerson).encode('utf-8'))
self.server.GETbusy=False self.server.GETbusy=False
return return
personKey = personKeyLookup(self.server.domain,self.path,self.server.baseDir) personKey = personKeyLookup(self.server.domain,self.path, \
self.server.baseDir)
if personKey: if personKey:
self._set_headers('text/html; charset=utf-8') self._set_headers('text/html; charset=utf-8')
self.wfile.write(personKey.encode('utf-8')) self.wfile.write(personKey.encode('utf-8'))
@ -228,13 +236,16 @@ class PubServer(BaseHTTPRequestHandler):
currSessionTime=int(time.time()) currSessionTime=int(time.time())
if currSessionTime-self.server.sessionLastUpdate>1200: if currSessionTime-self.server.sessionLastUpdate>1200:
self.server.sessionLastUpdate=currSessionTime self.server.sessionLastUpdate=currSessionTime
self.server.session = createSession(self.server.domain,self.server.port,self.server.useTor) self.server.session = \
createSession(self.server.domain,self.server.port, \
self.server.useTor)
print('**************** POST started new session') print('**************** POST started new session')
print('**************** POST get actor url from '+self.server.baseDir) print('**************** POST get actor url from '+self.server.baseDir)
personUrl=messageJson['actor'] personUrl=messageJson['actor']
print('**************** POST get public key of '+personUrl+' from '+self.server.baseDir) print('**************** POST get public key of '+personUrl+' from '+self.server.baseDir)
pubKey=getPersonPubKey(self.server.session,personUrl,self.server.personCache) pubKey=getPersonPubKey(self.server.session,personUrl, \
self.server.personCache)
if not pubKey: if not pubKey:
print('**************** POST no sender public key') print('**************** POST no sender public key')
self.send_response(401) self.send_response(401)
@ -242,14 +253,16 @@ class PubServer(BaseHTTPRequestHandler):
self.server.POSTbusy=False self.server.POSTbusy=False
return return
print('**************** POST check signature') print('**************** POST check signature')
if not verifyPostHeaders(self.server.https, pubKey, self.headers, '/inbox' ,False, json.dumps(messageJson)): if not verifyPostHeaders(self.server.https, pubKey, self.headers, \
'/inbox' ,False, json.dumps(messageJson)):
print('**************** POST signature verification failed') print('**************** POST signature verification failed')
self.send_response(401) self.send_response(401)
self.end_headers() self.end_headers()
self.server.POSTbusy=False self.server.POSTbusy=False
return return
print('**************** POST valid') print('**************** POST valid')
if receiveFollowRequest(self.server.baseDir,messageJson,self.server.federationList): if receiveFollowRequest(self.server.baseDir,messageJson, \
self.server.federationList):
self.send_response(200) self.send_response(200)
self.end_headers() self.end_headers()
self.server.POSTbusy=False self.server.POSTbusy=False

View File

@ -85,10 +85,10 @@ setBio(baseDir,username,domain,'Some personal info')
#pprint(outboxJson) #pprint(outboxJson)
#testPostMessageBetweenServers() #testPostMessageBetweenServers()
runDaemon(domain,port,https,federationList,useTor) #runDaemon(domain,port,https,federationList,useTor)
#testHttpsig() #testHttpsig()
sys.exit() #sys.exit()
#pprint(person) #pprint(person)
#print('\n') #print('\n')
@ -110,5 +110,5 @@ wfResult = json.dumps(wfRequest, indent=4, sort_keys=True)
maxMentions=10 maxMentions=10
maxEmoji=10 maxEmoji=10
maxAttachments=5 maxAttachments=5
userPosts = getPosts(session,personUrl,10,maxMentions,maxEmoji,maxAttachments,federationList,personCache) userPosts = getPosts(session,personUrl,30,maxMentions,maxEmoji,maxAttachments,federationList,personCache)
#print(str(userPosts)) #print(str(userPosts))

View File

@ -237,8 +237,8 @@ def receiveFollowRequest(baseDir: str,messageJson: {},federationList: []) -> boo
def sendFollowRequest(baseDir: str,username: str,domain: str,port: int,https: bool, \ def sendFollowRequest(baseDir: str,username: str,domain: str,port: int,https: bool, \
followUsername: str,followDomain: str,followPort: bool,followHttps: bool, \ followUsername: str,followDomain: str,followPort: bool,followHttps: bool, \
federationList: []): federationList: []) -> {}:
"""Sends a follow request """Gets the json object for sending a follow request
""" """
if not domainPermitted(followDomain,federationList): if not domainPermitted(followDomain,federationList):
return None return None
@ -268,3 +268,4 @@ def sendFollowRequest(baseDir: str,username: str,domain: str,port: int,https: bo
if ccUrl: if ccUrl:
if len(ccUrl)>0: if len(ccUrl)>0:
newFollow['cc']=ccUrl newFollow['cc']=ccUrl
return newFollow

View File

@ -15,7 +15,9 @@ from requests.auth import AuthBase
import base64 import base64
import json import json
def signPostHeaders(privateKeyPem: str, username: str, domain: str, port: int,path: str, https: bool, messageBodyJson: {}) -> str: def signPostHeaders(privateKeyPem: str, username: str, domain: str, \
port: int,path: str, \
https: bool, messageBodyJson: {}) -> str:
"""Returns a raw signature string that can be plugged into a header and """Returns a raw signature string that can be plugged into a header and
used to verify the authenticity of an HTTP transmission. used to verify the authenticity of an HTTP transmission.
""" """
@ -30,7 +32,8 @@ def signPostHeaders(privateKeyPem: str, username: str, domain: str, port: int,pa
if not messageBodyJson: if not messageBodyJson:
headers = {'host': domain} headers = {'host': domain}
else: else:
bodyDigest = base64.b64encode(SHA256.new(messageBodyJson.encode()).digest()) bodyDigest = \
base64.b64encode(SHA256.new(messageBodyJson.encode()).digest())
headers = {'host': domain, 'digest': f'SHA-256={bodyDigest}'} headers = {'host': domain, 'digest': f'SHA-256={bodyDigest}'}
privateKeyPem = RSA.import_key(privateKeyPem) privateKeyPem = RSA.import_key(privateKeyPem)
headers.update({ headers.update({
@ -59,7 +62,9 @@ def signPostHeaders(privateKeyPem: str, username: str, domain: str, port: int,pa
[f'{k}="{v}"' for k, v in signatureDict.items()]) [f'{k}="{v}"' for k, v in signatureDict.items()])
return signatureHeader return signatureHeader
def createSignedHeader(privateKeyPem: str,username: str,domain: str,port: int,path: str,https: bool,withDigest: bool,messageBodyJson: {}) -> {}: def createSignedHeader(privateKeyPem: str,username: str,domain: str,port: int, \
path: str,https: bool,withDigest: bool, \
messageBodyJson: {}) -> {}:
headerDomain=domain headerDomain=domain
if port!=80 and port!=443: if port!=80 and port!=443:
@ -69,15 +74,19 @@ def createSignedHeader(privateKeyPem: str,username: str,domain: str,port: int,pa
headers = {'host': headerDomain} headers = {'host': headerDomain}
else: else:
messageBodyJsonStr=json.dumps(messageBodyJson) messageBodyJsonStr=json.dumps(messageBodyJson)
bodyDigest = base64.b64encode(SHA256.new(messageBodyJsonStr.encode()).digest()) bodyDigest = \
base64.b64encode(SHA256.new(messageBodyJsonStr.encode()).digest())
headers = {'host': headerDomain, 'digest': f'SHA-256={bodyDigest}'} headers = {'host': headerDomain, 'digest': f'SHA-256={bodyDigest}'}
path='/inbox' path='/inbox'
signatureHeader = signPostHeaders(privateKeyPem, username, domain, port, path, https, None) signatureHeader = signPostHeaders(privateKeyPem, username, domain, port, \
path, https, None)
headers['signature'] = signatureHeader headers['signature'] = signatureHeader
headers['Content-type'] = 'application/json' headers['Content-type'] = 'application/json'
return headers return headers
def verifyPostHeaders(https: bool, publicKeyPem: str, headers: dict, path: str, GETmethod: bool, messageBodyJsonStr: str) -> bool: def verifyPostHeaders(https: bool, publicKeyPem: str, headers: dict, \
path: str, GETmethod: bool, \
messageBodyJsonStr: str) -> bool:
"""Returns true or false depending on if the key that we plugged in here """Returns true or false depending on if the key that we plugged in here
validates against the headers, method, and path. validates against the headers, method, and path.
publicKeyPem - the public key from an rsa key pair publicKeyPem - the public key from an rsa key pair
@ -111,7 +120,8 @@ def verifyPostHeaders(https: bool, publicKeyPem: str, headers: dict, path: str,
signedHeaderList.append( signedHeaderList.append(
f'(request-target): {method.lower()} {path}') f'(request-target): {method.lower()} {path}')
elif signedHeader == 'digest': elif signedHeader == 'digest':
bodyDigest = base64.b64encode(SHA256.new(messageBodyJsonStr.encode()).digest()) bodyDigest = \
base64.b64encode(SHA256.new(messageBodyJsonStr.encode()).digest())
signedHeaderList.append(f'digest: SHA-256={bodyDigest}') signedHeaderList.append(f'digest: SHA-256={bodyDigest}')
else: else:
signedHeaderList.append( signedHeaderList.append(

View File

@ -42,44 +42,10 @@ def inboxPermittedMessage(domain: str,messageJson: {},federationList: []) -> boo
return True return True
def receivePublicMessage(message: {}) -> bool: def validPublishedDate(published) -> bool:
print("TODO")
def validPublishedDate(published):
currTime=datetime.datetime.utcnow() currTime=datetime.datetime.utcnow()
pubDate=datetime.datetime.strptime(published,"%Y-%m-%dT%H:%M:%SZ") pubDate=datetime.datetime.strptime(published,"%Y-%m-%dT%H:%M:%SZ")
daysSincePublished = (currTime - pubTime).days daysSincePublished = (currTime - pubTime).days
if daysSincePublished>30: if daysSincePublished>30:
return False return False
return True return True
def receiveMessage(message: {},baseDir: str):
if not message.get('type'):
return
if message['type']!='Create':
return
if not message.get('published'):
return
# is the message too old?
if not validPublishedDate(message['published']):
return
if not message.get('to'):
return
if not message.get('id'):
return
for recipient in message['to']:
if recipient.endswith('/activitystreams#Public'):
receivePublicMessage(message)
continue
username=''
domain=''
messageId=message['id'].replace('/','_')
handle=username.lower()+'@'+domain.lower()
if not os.path.isdir(baseDir+'/accounts/'+handle):
os.mkdir(baseDir+'/accounts/'+handle)
if not os.path.isdir(baseDir+'/accounts/'+handle+'/inbox'):
os.mkdir(baseDir+'/accounts/'+handle+'/inbox')
filename=baseDir+'/accounts/'+handle+'/inbox/'+messageId+'.json'
with open(filename, 'w') as fp:
commentjson.dump(personJson, fp, indent=4, sort_keys=False)

View File

@ -10,7 +10,8 @@ import json
import commentjson import commentjson
from utils import urlPermitted from utils import urlPermitted
def like(baseDir: str,federationList: [],username: str,domain: str,port: int,toUrl: str,ccUrl: str,https: bool,objectUrl: str,saveToFile: bool) -> {}: def like(baseDir: str,federationList: [],username: str,domain: str,port: int, \
toUrl: str,ccUrl: str,https: bool,objectUrl: str,saveToFile: bool) -> {}:
"""Creates a like """Creates a like
Typically toUrl will be a followers collection Typically toUrl will be a followers collection
and ccUrl might be a specific person whose post was liked and ccUrl might be a specific person whose post was liked
@ -42,7 +43,10 @@ def like(baseDir: str,federationList: [],username: str,domain: str,port: int,toU
# TODO update likes collection # TODO update likes collection
return newLike return newLike
def likePost(baseDir: str,federationList: [],username: str, domain: str, port: int, https: bool, likeUsername: str, likeDomain: str, likePort: int, likeStatusNumber: int, likeHttps: bool,saveToFile: bool) -> {}: def likePost(baseDir: str,federationList: [], \
username: str, domain: str, port: int, https: bool, \n
likeUsername: str, likeDomain: str, likePort: int, likeHttps: bool, \n
likeStatusNumber: int,saveToFile: bool) -> {}:
"""Likes a given status post """Likes a given status post
""" """
prefix='https' prefix='https'

View File

@ -21,7 +21,8 @@ def generateRSAKey() -> (str,str):
publicKeyPem = key.publickey().exportKey("PEM").decode("utf-8") publicKeyPem = key.publickey().exportKey("PEM").decode("utf-8")
return privateKeyPem,publicKeyPem return privateKeyPem,publicKeyPem
def createPerson(baseDir: str,username: str,domain: str,port: int,https: bool, saveToFile: bool) -> (str,str,{},{}): def createPerson(baseDir: str,username: str,domain: str,port: int, \
https: bool, saveToFile: bool) -> (str,str,{},{}):
"""Returns the private key, public key, actor and webfinger endpoint """Returns the private key, public key, actor and webfinger endpoint
""" """
prefix='https' prefix='https'
@ -29,7 +30,8 @@ def createPerson(baseDir: str,username: str,domain: str,port: int,https: bool, s
prefix='http' prefix='http'
privateKeyPem,publicKeyPem=generateRSAKey() privateKeyPem,publicKeyPem=generateRSAKey()
webfingerEndpoint=createWebfingerEndpoint(username,domain,port,https,publicKeyPem) webfingerEndpoint= \
createWebfingerEndpoint(username,domain,port,https,publicKeyPem)
if saveToFile: if saveToFile:
storeWebfingerEndpoint(username,domain,baseDir,webfingerEndpoint) storeWebfingerEndpoint(username,domain,baseDir,webfingerEndpoint)
@ -140,7 +142,10 @@ def personKeyLookup(domain: str,path: str,baseDir: str) -> str:
def personLookup(domain: str,path: str,baseDir: str) -> {}: def personLookup(domain: str,path: str,baseDir: str) -> {}:
"""Lookup the person for an given username """Lookup the person for an given username
""" """
notPersonLookup=['/inbox','/outbox','/outboxarchive','/followers','/following','/featured','.png','.jpg','.gif','.mpv','#main-key','/main-key'] notPersonLookup=['/inbox','/outbox','/outboxarchive', \
'/followers','/following','/featured', \
'.png','.jpg','.gif','.mpv', \
'#main-key','/main-key']
for ending in notPersonLookup: for ending in notPersonLookup:
if path.endswith(ending): if path.endswith(ending):
return None return None
@ -164,7 +169,8 @@ def personLookup(domain: str,path: str,baseDir: str) -> {}:
personJson=commentjson.load(fp) personJson=commentjson.load(fp)
return personJson return personJson
def personOutboxJson(baseDir: str,domain: str,port: int,path: str,https: bool,noOfItems: int) -> []: def personOutboxJson(baseDir: str,domain: str,port: int,path: str, \
https: bool,noOfItems: int) -> []:
"""Obtain the outbox feed for the given person """Obtain the outbox feed for the given person
""" """
if not '/outbox' in path: if not '/outbox' in path:
@ -198,9 +204,11 @@ def personOutboxJson(baseDir: str,domain: str,port: int,path: str,https: bool,no
return None return None
if not validUsername(username): if not validUsername(username):
return None return None
return createOutbox(baseDir,username,domain,port,https,noOfItems,headerOnly,pageNumber) return createOutbox(baseDir,username,domain,port,https, \
noOfItems,headerOnly,pageNumber)
def setPreferredUsername(baseDir: str,username: str, domain: str, preferredName: str) -> bool: def setPreferredUsername(baseDir: str,username: str, domain: str, \
preferredName: str) -> bool:
if len(preferredName)>32: if len(preferredName)>32:
return False return False
handle=username.lower()+'@'+domain.lower() handle=username.lower()+'@'+domain.lower()

View File

@ -118,7 +118,9 @@ def getPersonPubKey(session,personUrl: str,personCache: {}) -> str:
storePersonInCache(personUrl,personJson,personCache) storePersonInCache(personUrl,personJson,personCache)
return pubKey return pubKey
def getPosts(session,outboxUrl: str,maxPosts: int,maxMentions: int,maxEmoji: int,maxAttachments: int,federationList: [],personCache: {}) -> {}: def getPosts(session,outboxUrl: str,maxPosts: int,maxMentions: int, \
maxEmoji: int,maxAttachments: int,federationList: [], \
personCache: {}) -> {}:
personPosts={} personPosts={}
if not outboxUrl: if not outboxUrl:
return personPosts return personPosts
@ -234,7 +236,10 @@ def deleteAllPosts(username: str, domain: str,baseDir: str) -> None:
except Exception as e: except Exception as e:
print(e) print(e)
def createPostBase(baseDir: str,username: str, domain: str, port: int,toUrl: str, ccUrl: str, https: bool, content: str, followersOnly: bool, saveToFile: bool, inReplyTo=None, inReplyToAtomUri=None, subject=None) -> {}: def createPostBase(baseDir: str,username: str, domain: str, port: int, \
toUrl: str, ccUrl: str, https: bool, content: str, \
followersOnly: bool, saveToFile: bool, \
inReplyTo=None, inReplyToAtomUri=None, subject=None) -> {}:
"""Creates a message """Creates a message
""" """
prefix='https' prefix='https'
@ -308,21 +313,29 @@ def createPostBase(baseDir: str,username: str, domain: str, port: int,toUrl: str
commentjson.dump(newPost, fp, indent=4, sort_keys=False) commentjson.dump(newPost, fp, indent=4, sort_keys=False)
return newPost return newPost
def createPublicPost(baseDir: str,username: str, domain: str, port: int,https: bool, content: str, followersOnly: bool, saveToFile: bool, inReplyTo=None, inReplyToAtomUri=None, subject=None) -> {}: def createPublicPost(baseDir: str,username: str, domain: str, port: int,https: bool, \
content: str, followersOnly: bool, saveToFile: bool, \
inReplyTo=None, inReplyToAtomUri=None, subject=None) -> {}:
"""Public post to the outbox """Public post to the outbox
""" """
prefix='https' prefix='https'
if not https: if not https:
prefix='http' prefix='http'
return createPostBase(baseDir,username, domain, port,'https://www.w3.org/ns/activitystreams#Public', prefix+'://'+domain+'/users/'+username+'/followers', https, content, followersOnly, saveToFile, inReplyTo, inReplyToAtomUri, subject) return createPostBase(baseDir,username, domain, port, \
'https://www.w3.org/ns/activitystreams#Public', \
prefix+'://'+domain+'/users/'+username+'/followers', \
https, content, followersOnly, saveToFile, \
inReplyTo, inReplyToAtomUri, subject)
def threadSendPost(session,postJsonObject: {},federationList: [],inboxUrl: str,baseDir: str,signatureHeaderJson: {},postLog: []) -> None: def threadSendPost(session,postJsonObject: {},federationList: [],inboxUrl: str, \
baseDir: str,signatureHeaderJson: {},postLog: []) -> None:
"""Sends a post with exponential backoff """Sends a post with exponential backoff
""" """
tries=0 tries=0
backoffTime=60 backoffTime=60
for attempt in range(20): for attempt in range(20):
postResult = postJson(session,postJsonObject,federationList,inboxUrl,signatureHeaderJson) postResult = postJson(session,postJsonObject,federationList, \
inboxUrl,signatureHeaderJson)
if postResult: if postResult:
postLog.append(postJsonObject['published']+' '+postResult+'\n') postLog.append(postJsonObject['published']+' '+postResult+'\n')
# keep the length of the log finite # keep the length of the log finite
@ -339,7 +352,12 @@ def threadSendPost(session,postJsonObject: {},federationList: [],inboxUrl: str,b
time.sleep(backoffTime) time.sleep(backoffTime)
backoffTime *= 2 backoffTime *= 2
def sendPost(session,baseDir: str,username: str, domain: str, port: int, toUsername: str, toDomain: str, toPort: int, cc: str, https: bool, content: str, followersOnly: bool, saveToFile: bool, federationList: [], sendThreads: [], postLog: [], cachedWebfingers: {},personCache: {},inReplyTo=None, inReplyToAtomUri=None, subject=None) -> int: def sendPost(session,baseDir: str,username: str, domain: str, port: int, \
toUsername: str, toDomain: str, toPort: int, cc: str, \
https: bool, content: str, followersOnly: bool, \
saveToFile: bool, federationList: [], sendThreads: [], \
postLog: [], cachedWebfingers: {},personCache: {}, \
inReplyTo=None, inReplyToAtomUri=None, subject=None) -> int:
"""Post to another inbox """Post to another inbox
""" """
prefix='https' prefix='https'
@ -359,7 +377,8 @@ def sendPost(session,baseDir: str,username: str, domain: str, port: int, toUsern
return 1 return 1
# get the actor inbox for the To handle # get the actor inbox for the To handle
inboxUrl,pubKeyId,pubKey,toPersonId = getPersonBox(session,wfRequest,personCache,'inbox') inboxUrl,pubKeyId,pubKey,toPersonId = \
getPersonBox(session,wfRequest,personCache,'inbox')
if not inboxUrl: if not inboxUrl:
return 2 return 2
if not pubKey: if not pubKey:
@ -367,7 +386,11 @@ def sendPost(session,baseDir: str,username: str, domain: str, port: int, toUsern
if not toPersonId: if not toPersonId:
return 4 return 4
postJsonObject=createPostBase(baseDir,username,domain,port,toPersonId,cc,https,content,followersOnly,saveToFile,inReplyTo,inReplyToAtomUri,subject) postJsonObject=createPostBase(baseDir,username,domain,port, \
toPersonId,cc,https,content, \
followersOnly,saveToFile, \
inReplyTo,inReplyToAtomUri, \
subject)
# get the senders private key # get the senders private key
privateKeyPem=getPersonKey(username,domain,baseDir,'private') privateKeyPem=getPersonKey(username,domain,baseDir,'private')
@ -375,18 +398,26 @@ def sendPost(session,baseDir: str,username: str, domain: str, port: int, toUsern
return 5 return 5
# construct the http header # construct the http header
signatureHeaderJson = createSignedHeader(privateKeyPem, username, domain, port, '/inbox', https, withDigest, postJsonObject) signatureHeaderJson = \
createSignedHeader(privateKeyPem, username, domain, port, \
'/inbox', https, withDigest, postJsonObject)
# Keep the number of threads being used small # Keep the number of threads being used small
while len(sendThreads)>10: while len(sendThreads)>10:
sendThreads[0].kill() sendThreads[0].kill()
sendThreads.pop(0) sendThreads.pop(0)
thr = threadWithTrace(target=threadSendPost,args=(session,postJsonObject.copy(),federationList,inboxUrl,baseDir,signatureHeaderJson.copy(),postLog),daemon=True) thr = threadWithTrace(target=threadSendPost,args=(session, \
postJsonObject.copy(), \
federationList, \
inboxUrl,baseDir, \
signatureHeaderJson.copy(), \
postLog),daemon=True)
sendThreads.append(thr) sendThreads.append(thr)
thr.start() thr.start()
return 0 return 0
def createOutbox(baseDir: str,username: str,domain: str,port: int,https: bool,itemsPerPage: int,headerOnly: bool,pageNumber=None) -> {}: def createOutbox(baseDir: str,username: str,domain: str,port: int,https: bool, \
itemsPerPage: int,headerOnly: bool,pageNumber=None) -> {}:
"""Constructs the outbox feed """Constructs the outbox feed
""" """
prefix='https' prefix='https'
@ -484,7 +515,8 @@ def createOutbox(baseDir: str,username: str,domain: str,port: int,https: bool,it
return outboxHeader return outboxHeader
return outboxItems return outboxItems
def archivePosts(username: str,domain: str,baseDir: str,maxPostsInOutbox=256) -> None: def archivePosts(username: str,domain: str,baseDir: str, \
maxPostsInOutbox=256) -> None:
"""Retain a maximum number of posts within the outbox """Retain a maximum number of posts within the outbox
Move any others to an archive directory Move any others to an archive directory
""" """

View File

@ -21,10 +21,12 @@ def parseHandle(handle: str) -> (str,str):
if '.' not in handle: if '.' not in handle:
return None, None return None, None
if '/@' in handle: if '/@' in handle:
domain, username = handle.replace('https://','').replace('http://','').split('/@') domain, username = \
handle.replace('https://','').replace('http://','').split('/@')
else: else:
if '/users/' in handle: if '/users/' in handle:
domain, username = handle.replace('https://','').replace('http://','').split('/users/') domain, username = \
handle.replace('https://','').replace('http://','').split('/users/')
else: else:
if '@' in handle: if '@' in handle:
username, domain = handle.split('@') username, domain = handle.split('@')
@ -67,7 +69,8 @@ def generateMagicKey(publicKeyPem) -> str:
pubexp = base64.urlsafe_b64encode(number.long_to_bytes(privkey.e)).decode("utf-8") pubexp = base64.urlsafe_b64encode(number.long_to_bytes(privkey.e)).decode("utf-8")
return f"data:application/magic-public-key,RSA.{mod}.{pubexp}" return f"data:application/magic-public-key,RSA.{mod}.{pubexp}"
def storeWebfingerEndpoint(username: str,domain: str,baseDir: str,wfJson: {}) -> bool: def storeWebfingerEndpoint(username: str,domain: str,baseDir: str, \
wfJson: {}) -> bool:
"""Stores webfinger endpoint for a user to a file """Stores webfinger endpoint for a user to a file
""" """
handle=username+'@'+domain handle=username+'@'+domain
@ -79,7 +82,8 @@ def storeWebfingerEndpoint(username: str,domain: str,baseDir: str,wfJson: {}) ->
commentjson.dump(wfJson, fp, indent=4, sort_keys=False) commentjson.dump(wfJson, fp, indent=4, sort_keys=False)
return True return True
def createWebfingerEndpoint(username: str,domain: str,port: int,https: bool,publicKeyPem) -> {}: def createWebfingerEndpoint(username: str,domain: str,port: int, \
https: bool,publicKeyPem) -> {}:
"""Creates a webfinger endpoint for a user """Creates a webfinger endpoint for a user
""" """
prefix='https' prefix='https'