From a98facaf33f5366e822bd0859645dfa9a980516c Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 16 Jul 2019 11:19:04 +0100 Subject: [PATCH] Fixing c2s --- daemon.py | 50 +++++++++++++++------- epicyon.py | 6 ++- media.py | 8 ++-- posts.py | 117 +++++++++++++++++++++++++++++++++++++++++++++------ session.py | 2 + tests.py | 89 ++++++++++++++++++++++++++++++++++++++- webfinger.py | 7 +++ 7 files changed, 244 insertions(+), 35 deletions(-) diff --git a/daemon.py b/daemon.py index 1fa4dcbe..dbaa19e7 100644 --- a/daemon.py +++ b/daemon.py @@ -124,15 +124,17 @@ class PubServer(BaseHTTPRequestHandler): messageJson= \ outboxMessageCreateWrap(self.server.httpPrefix, \ self.postToNickname, \ - self.server.domain,messageJson) + self.server.domain, \ + self.server.port, \ + messageJson) if messageJson['type']=='Create': if not (messageJson.get('id') and \ messageJson.get('type') and \ messageJson.get('actor') and \ messageJson.get('object') and \ - messageJson.get('atomUri') and \ messageJson.get('to')): if self.server.debug: + pprint(messageJson) print('DEBUG: POST to outbox - Create does not have the required parameters') return False # https://www.w3.org/TR/activitypub/#create-activity-outbox @@ -150,23 +152,31 @@ class PubServer(BaseHTTPRequestHandler): postId=messageJson['id'] else: postId=None - savePostToBox(self.server.baseDir,postId, \ + if self.server.debug: + pprint(messageJson) + savePostToBox(self.server.baseDir, \ + self.server.httpPrefix, \ + postId, \ self.postToNickname, \ self.server.domain,messageJson,'outbox') + if not self.server.session: + self.server.session= \ + createSession(self.server.domain,self.server.port,self.server.useTor) if self.server.debug: print('DEBUG: sending c2s post to followers') sendToFollowers(self.server.session,self.server.baseDir, \ - self.postToNickname,self.server.domain, \ - self.server.port, \ - self.server.httpPrefix, \ - self.server.federationList, \ - self.server.sendThreads, \ - self.server.postLog, \ - self.server.cachedWebfingers, \ - self.server.personCache, \ - messageJson,self.server.debug) + self.postToNickname,self.server.domain, \ + self.server.port, \ + self.server.httpPrefix, \ + self.server.federationList, \ + self.server.sendThreads, \ + self.server.postLog, \ + self.server.cachedWebfingers, \ + self.server.personCache, \ + messageJson,self.server.debug) if self.server.debug: print('DEBUG: sending c2s post to named addresses') + print('c2s sender: '+self.postToNickname+'@'+self.server.domain+':'+str(self.server.port)) sendToNamedAddresses(self.server.session,self.server.baseDir, \ self.postToNickname,self.server.domain, \ self.server.port, \ @@ -580,7 +590,7 @@ class PubServer(BaseHTTPRequestHandler): if '/users/' in self.path: if self._isAuthorized(): self.outboxAuthenticated=True - pathUsersSection=path.split('/users/')[1] + pathUsersSection=self.path.split('/users/')[1] self.postToNickname=pathUsersSection.split('/')[0] if not self.outboxAuthenticated: self.send_response(405) @@ -617,9 +627,16 @@ class PubServer(BaseHTTPRequestHandler): # https://www.w3.org/TR/activitypub/#object-without-create if self.outboxAuthenticated: - if self._postToOutbox(messageJson): - self.send_header('Location', \ - messageJson['object']['atomUri']) + if self._postToOutbox(messageJson): + if messageJson.get('object'): + #self.send_header('Location', \ + self.headers['Location']= \ + messageJson['object']['id'].replace('/activity','') + else: + if messageJson.get('id'): + #self.send_header('Location', \ + self.headers['Location']= \ + messageJson['id'].replace('/activity','') self.send_response(201) self.end_headers() self.server.POSTbusy=False @@ -638,6 +655,7 @@ class PubServer(BaseHTTPRequestHandler): self.path=='/sharedInbox': if not inboxMessageHasParams(messageJson): if self.server.debug: + pprint(messageJson) print("DEBUG: inbox message doesn't have the required parameters") self.send_response(403) self.end_headers() diff --git a/epicyon.py b/epicyon.py index 81f9471a..5d8caf6f 100644 --- a/epicyon.py +++ b/epicyon.py @@ -52,6 +52,7 @@ from follow import unfollowerOfPerson from follow import getFollowersOfPerson from tests import testPostMessageBetweenServers from tests import testFollowBetweenServers +from tests import testClientToServer from tests import runAllTests from config import setConfigParam from config import getConfigParam @@ -213,8 +214,9 @@ if args.tests: if args.testsnetwork: print('Network Tests') - testPostMessageBetweenServers() - testFollowBetweenServers() + testClientToServer() + #testPostMessageBetweenServers() + #testFollowBetweenServers() sys.exit() if args.posts: diff --git a/media.py b/media.py index 06a58898..5ea2ca5a 100644 --- a/media.py +++ b/media.py @@ -64,9 +64,10 @@ def attachImage(baseDir: str,httpPrefix: str,domain: str,port: int, \ domain=domain+':'+str(port) mPath=getMediaPath() - createMediaDirs(baseDir,mPath) mediaPath=mPath+'/'+createPassword(32)+'.'+fileExtension - mediaFilename=baseDir+'/'+mediaPath + if baseDir: + createMediaDirs(baseDir,mPath) + mediaFilename=baseDir+'/'+mediaPath attachmentJson={ 'mediaType': mediaType, @@ -78,7 +79,8 @@ def attachImage(baseDir: str,httpPrefix: str,domain: str,port: int, \ attachmentJson['blurhash']=getImageHash(imageFilename) postJson['attachment']=[attachmentJson] - copyfile(imageFilename,mediaFilename) + if baseDir: + copyfile(imageFilename,mediaFilename) return postJson diff --git a/posts.py b/posts.py index b4850d0a..fb6a5a94 100644 --- a/posts.py +++ b/posts.py @@ -38,6 +38,7 @@ from capabilities import getOcapFilename from capabilities import capabilitiesUpdate from media import attachImage from content import addMentions +from auth import createBasicAuthHeader try: from BeautifulSoup import BeautifulSoup except ImportError: @@ -318,9 +319,10 @@ def createPostBase(baseDir: str,nickname: str, domain: str, port: int, \ inReplyTo=None, inReplyToAtomUri=None, subject=None) -> {}: """Creates a message """ - # convert content to html - content=addMentions(baseDir,httpPrefix, \ - nickname,domain,content) + if not clientToServer: + # convert content to html + content=addMentions(baseDir,httpPrefix, \ + nickname,domain,content) if port!=80 and port!=443: domain=domain+':'+str(port) @@ -444,12 +446,16 @@ def createPostBase(baseDir: str,nickname: str, domain: str, port: int, \ nickname,domain,newPost,'outbox') return newPost -def outboxMessageCreateWrap(httpPrefix: str,nickname: str,domain: str, \ +def outboxMessageCreateWrap(httpPrefix: str, \ + nickname: str,domain: str,port: int, \ messageJson: {}) -> {}: """Wraps a received message in a Create https://www.w3.org/TR/activitypub/#object-without-create """ + if port!=80 and port!=443: + if ':' not in domain: + domain=domain+':'+str(port) statusNumber,published = getStatusNumber() if messageJson.get('published'): published = messageJson['published'] @@ -458,7 +464,7 @@ def outboxMessageCreateWrap(httpPrefix: str,nickname: str,domain: str, \ if messageJson.get('cc'): cc=messageJson['cc'] # TODO - capabilityUrl='' + capabilityUrl=[] newPost = { 'id': newPostId+'/activity', 'capability': capabilityUrl, @@ -605,7 +611,7 @@ def sendPost(session,baseDir: str,nickname: str, domain: str, port: int, \ 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 + # then send to the shared inbox indead of the individual inbox if nickname=='capabilities': inboxUrl=capabilityAcquisition if not capabilityAcquisition: @@ -658,6 +664,76 @@ def sendPost(session,baseDir: str,nickname: str, domain: str, port: int, \ thr.start() return 0 +def sendPostViaServer(session,fromNickname: str,password: str, \ + fromDomain: str, fromPort: int, \ + toNickname: str, toDomain: str, toPort: int, cc: str, \ + httpPrefix: str, content: str, followersOnly: bool, \ + attachImageFilename: str,imageDescription: str,useBlurhash: bool, \ + cachedWebfingers: {},personCache: {}, \ + debug=False,inReplyTo=None,inReplyToAtomUri=None,subject=None) -> int: + """Send a post via a proxy (c2s) + """ + withDigest=True + + if toPort!=80 and toPort!=443: + if ':' not in fromDomain: + fromDomain=fromDomain+':'+str(fromPort) + + handle=httpPrefix+'://'+fromDomain+'/@'+fromNickname + + # lookup the inbox for the To handle + wfRequest = webfingerHandle(session,handle,httpPrefix,cachedWebfingers) + if not wfRequest: + if debug: + print('DEBUG: webfinger failed for '+handle) + return 1 + + postToBox='outbox' + + # get the actor inbox for the To handle + inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition = \ + getPersonBox(session,wfRequest,personCache,postToBox) + + if not inboxUrl: + if debug: + print('DEBUG: No '+postToBox+' was found for '+handle) + return 3 + if not fromPersonId: + if debug: + print('DEBUG: No actor was found for '+handle) + return 4 + + # Get the json for the c2s post, not saving anything to file + # Note that baseDir is set to None + saveToFile=False + clientToServer=True + toDomainFull=toDomain + if toPort!=80 and toDomain!=443: + toDomainFull=toDomain+':'+str(toPort) + toPersonId=httpPrefix+'://'+toDomainFull+'/users/'+toNickname + postJsonObject = \ + createPostBase(None, \ + fromNickname,fromDomain,fromPort, \ + toPersonId,cc,httpPrefix,content, \ + followersOnly,saveToFile,clientToServer, \ + attachImageFilename,imageDescription,useBlurhash, \ + inReplyTo,inReplyToAtomUri,subject) + + authHeader=createBasicAuthHeader(fromNickname,password) + headers = {'host': fromDomain, \ + 'Content-type': 'application/json', \ + 'Authorization': authHeader} + postResult = \ + postJson(session,postJsonObject,[],inboxUrl,headers,"inbox:write") + if not postResult: + if debug: + print('DEBUG: POST failed for c2s to '+inboxUrl) + return 5 + + if debug: + print('DEBUG: c2s POST success') + return 0 + def groupFollowersByDomain(baseDir :str,nickname :str,domain :str) -> {}: """Returns a dictionary with followers grouped by domain """ @@ -686,6 +762,9 @@ def sendSignedJson(postJsonObject: {},session,baseDir: str, \ personCache: {}, debug: bool) -> int: """Sends a signed json object to an inbox/outbox """ + if not session: + print('WARN: No session specified for sendSignedJson') + return 8 withDigest=True sharedInbox=False @@ -773,13 +852,13 @@ def sendToNamedAddresses(session,baseDir: str, \ postJsonObject: {},debug: bool) -> None: """sends a post to the specific named addresses in to/cc """ - if port!=80 and port!=443: - domain=domain+':'+str(port) - + if not session: + print('WARN: No session for sendToNamedAddresses') + return if not postJsonObject.get('object'): - return False + return if not postJsonObject['object'].get('to'): - return False + return recipients=[] recipientType=['to','cc'] @@ -793,7 +872,7 @@ def sendToNamedAddresses(session,baseDir: str, \ if not recipients: return if debug: - print('c2s sending to addresses: '+str(recipients)) + print('Sending individually addressed posts: '+str(recipients)) # this is after the message has arrived at the server clientToServer=False for address in recipients: @@ -804,7 +883,16 @@ def sendToNamedAddresses(session,baseDir: str, \ if not toDomain: continue if debug: - print('c2s sending from '+nickname+'@'+domain+' to '+toNickname+'@'+toDomain) + domainFull=domain + if port: + if port!=80 and port!=443: + domainFull=domain+':'+str(port) + toDomainFull=toDomain + if toPort: + if toPort!=80 and toPort!=443: + toDomainFull=toDomain+':'+str(toPort) + print('Post sending s2s: '+nickname+'@'+domainFull+' to '+toNickname+'@'+toDomainFull) + cc=[] sendSignedJson(postJsonObject,session,baseDir, \ nickname,domain,port, \ toNickname,toDomain,toPort, \ @@ -821,6 +909,9 @@ def sendToFollowers(session,baseDir: str, \ postJsonObject: {},debug: bool) -> None: """sends a post to the followers of the given nickname """ + if not session: + print('WARN: No session for sendToFollowers') + return if not postIsAddressedToFollowers(baseDir,nickname,domain, \ port,httpPrefix,postJsonObject): if debug: diff --git a/session.py b/session.py index 6f8e78c4..0400eaab 100644 --- a/session.py +++ b/session.py @@ -31,6 +31,8 @@ def getJson(session,url: str,headers: {},params: {}) -> {}: if params: sessionParams=params sessionHeaders['User-agent'] = "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 5 Build/LMY48B; wv)" + if not session: + print('WARN: no session specified for getJson') session.cookies.clear() try: result=session.get(url, headers=sessionHeaders, params=sessionParams) diff --git a/tests.py b/tests.py index 21af2d60..556d220f 100644 --- a/tests.py +++ b/tests.py @@ -29,6 +29,7 @@ from posts import noOfFollowersOnDomain from posts import groupFollowersByDomain from posts import sendCapabilitiesUpdate from posts import archivePostsForPerson +from posts import sendPostViaServer from follow import clearFollows from follow import clearFollowers from utils import followPerson @@ -136,7 +137,6 @@ def createServerAlice(path: str,domain: str,port: int,federationList: [], \ nickname='alice' httpPrefix='http' useTor=False - clientToServer=False password='alicepass' noreply=False nolike=False @@ -954,7 +954,94 @@ def testAuthentication(): os.chdir(currDir) shutil.rmtree(baseDir) + +def testClientToServer(): + print('Testing sending a post via c2s') + + global testServerAliceRunning + global testServerBobRunning + testServerAliceRunning = False + testServerBobRunning = False + + httpPrefix='http' + useTor=False + federationList=[] + + baseDir=os.getcwd() + if os.path.isdir(baseDir+'/.tests'): + shutil.rmtree(baseDir+'/.tests') + os.mkdir(baseDir+'/.tests') + + ocapAlways=False + + # create the servers + aliceDir=baseDir+'/.tests/alice' + aliceDomain='127.0.0.42' + alicePort=61935 + thrAlice = \ + threadWithTrace(target=createServerAlice, \ + args=(aliceDir,aliceDomain,alicePort, \ + federationList,False,False, \ + ocapAlways),daemon=True) + bobDir=baseDir+'/.tests/bob' + bobDomain='127.0.0.64' + bobPort=61936 + thrBob = \ + threadWithTrace(target=createServerBob, \ + args=(bobDir,bobDomain,bobPort, \ + federationList,False,False, \ + ocapAlways),daemon=True) + + thrAlice.start() + thrBob.start() + assert thrAlice.isAlive()==True + assert thrBob.isAlive()==True + + # wait for both servers to be running + ctr=0 + while not (testServerAliceRunning and testServerBobRunning): + time.sleep(1) + ctr+=1 + if ctr>60: + break + print('Alice online: '+str(testServerAliceRunning)) + print('Bob online: '+str(testServerBobRunning)) + + time.sleep(1) + + print('\n\n*******************************************************') + print('Alice sends to Bob via c2s') + + sessionAlice = createSession(aliceDomain,alicePort,useTor) + followersOnly=False + attachImageFilename=None + imageDescription=None + useBlurhash=False + cachedWebfingers={} + personCache={} + password='alicepass' + outboxPath=aliceDir+'/accounts/alice@'+aliceDomain+'/outbox' + assert len([name for name in os.listdir(outboxPath) if os.path.isfile(os.path.join(outboxPath, name))])==0 + sendResult= \ + sendPostViaServer(sessionAlice,'alice',password, \ + aliceDomain,alicePort, \ + 'bob',bobDomain,bobPort,None, \ + httpPrefix,'Sent from my ActivityPub client',followersOnly, \ + attachImageFilename,imageDescription,useBlurhash, \ + cachedWebfingers,personCache, \ + True,None,None,None) + print('sendResult: '+str(sendResult)) + assert sendResult==0 + + for i in range(30): + if os.path.isdir(outboxPath): + if len([name for name in os.listdir(outboxPath) if os.path.isfile(os.path.join(outboxPath, name))])==1: + break + time.sleep(1) + + assert len([name for name in os.listdir(outboxPath) if os.path.isfile(os.path.join(outboxPath, name))])==1 + def runAllTests(): print('Running tests...') testHttpsig() diff --git a/webfinger.py b/webfinger.py index 7c50f6eb..50c85418 100644 --- a/webfinger.py +++ b/webfinger.py @@ -37,6 +37,10 @@ def parseHandle(handle: str) -> (str,str): def webfingerHandle(session,handle: str,httpPrefix: str,cachedWebfingers: {}) -> {}: + if not session: + print('WARN: No session specified for webfingerHandle') + return None + nickname, domain = parseHandle(handle) if not nickname: return None @@ -49,6 +53,9 @@ def webfingerHandle(session,handle: str,httpPrefix: str,cachedWebfingers: {}) -> url = '{}://{}/.well-known/webfinger'.format(httpPrefix,domain) par = {'resource': 'acct:{}'.format(nickname+'@'+wfDomain)} hdr = {'Accept': 'application/jrd+json'} + #print('webfinger url: '+url) + #print('webfinger par: '+str(par)) + #print('webfinger hdr: '+str(hdr)) try: result = getJson(session, url, hdr, par) except: