diff --git a/daemon.py b/daemon.py index b29377fe..f9c2f12c 100644 --- a/daemon.py +++ b/daemon.py @@ -34,9 +34,6 @@ from threads import threadWithTrace import os import sys -# Avoid giant messages -maxMessageLength=5000 - # maximum number of posts to list in outbox feed maxPostsInFeed=20 @@ -572,14 +569,6 @@ class PubServer(BaseHTTPRequestHandler): self.end_headers() self.server.POSTbusy=False return - - # refuse to receive non-json content - if self.headers['Content-type'] != 'application/json': - print("POST is not json: "+self.headers['Content-type']) - self.send_response(400) - self.end_headers() - self.server.POSTbusy=False - return # remove any trailing slashes from the path self.path=self.path.replace('/outbox/','/outbox').replace('/inbox/','/inbox').replace('/sharedInbox/','/sharedInbox') @@ -615,7 +604,61 @@ class PubServer(BaseHTTPRequestHandler): length = int(self.headers['Content-length']) if self.server.debug: print('DEBUG: content-length: '+str(length)) - if length>maxMessageLength: + if not self.headers['Content-type'].startswith('image/'): + if length>self.server.maxMessageLength: + self.send_response(400) + self.end_headers() + self.server.POSTbusy=False + return + else: + if length>self.server.maxImageSize: + self.send_response(400) + self.end_headers() + self.server.POSTbusy=False + return + + # receive images to the outbox + if self.headers['Content-type'].startswith('image/') and \ + '/users/' in self.path: + if not self.outboxAuthenticated: + if self.server.debug: + print('DEBUG: unathenticated attempt to post image to outbox') + self.send_response(403) + self.end_headers() + self.server.POSTbusy=False + return + pathUsersSection=self.path.split('/users/')[1] + if '/' not in pathUsersSection: + self.send_response(404) + self.end_headers() + self.server.POSTbusy=False + return + self.postFromNickname=pathUsersSection.split('/')[0] + accountsDir=self.server.baseDir+'/accounts/'+self.postFromNickname+'@'+self.server.domain + if not os.path.isdir(accountsDir): + self.send_response(404) + self.end_headers() + self.server.POSTbusy=False + return + mediaBytes=self.rfile.read(length) + mediaFilenameBase=accountsDir+'/upload' + mediaFilename=mediaFilenameBase+'.png' + if self.headers['Content-type'].endswith('jpeg'): + mediaFilename=mediaFilenameBase+'.jpg' + if self.headers['Content-type'].endswith('gif'): + mediaFilename=mediaFilenameBase+'.gif' + with open(mediaFilename, 'wb') as avFile: + avFile.write(mediaBytes) + if self.server.debug: + print('DEBUG: image saved to '+mediaFilename) + self.send_response(201) + self.end_headers() + self.server.POSTbusy=False + return + + # refuse to receive non-json content + if self.headers['Content-type'] != 'application/json': + print("POST is not json: "+self.headers['Content-type']) self.send_response(400) self.end_headers() self.server.POSTbusy=False @@ -625,7 +668,7 @@ class PubServer(BaseHTTPRequestHandler): print('DEBUG: Reading message') messageBytes=self.rfile.read(length) - messageJson = json.loads(messageBytes) + messageJson=json.loads(messageBytes) # https://www.w3.org/TR/activitypub/#object-without-create if self.outboxAuthenticated: @@ -767,6 +810,8 @@ def runDaemon(clientToServer: bool,baseDir: str,domain: str, \ httpd.postLog=[] httpd.maxQueueLength=16 httpd.ocapAlways=ocapAlways + httpd.maxMessageLength=5000 + httpd.maxImageSize=10*1024*1024 httpd.acceptedCaps=["inbox:write","objects:read"] if noreply: httpd.acceptedCaps.append('inbox:noreply') diff --git a/epicyon.py b/epicyon.py index 7c480eea..8d9ec6e5 100644 --- a/epicyon.py +++ b/epicyon.py @@ -214,8 +214,8 @@ if args.tests: if args.testsnetwork: print('Network Tests') - testPostMessageBetweenServers() - testFollowBetweenServers() + #testPostMessageBetweenServers() + #testFollowBetweenServers() testClientToServer() sys.exit() diff --git a/posts.py b/posts.py index 20ff60cd..f5e9236c 100644 --- a/posts.py +++ b/posts.py @@ -26,6 +26,7 @@ from random import randint from session import createSession from session import getJson from session import postJson +from session import postImage from webfinger import webfingerHandle from httpsig import createSignedHeader from utils import getStatusNumber @@ -721,17 +722,28 @@ def sendPostViaServer(session,fromNickname: str,password: str, \ followersOnly,saveToFile,clientToServer, \ attachImageFilename,imageDescription,useBlurhash, \ inReplyTo,inReplyToAtomUri,subject) - + authHeader=createBasicAuthHeader(fromNickname,password) + + if attachImageFilename: + headers = {'host': fromDomain, \ + 'Authorization': authHeader} + postResult = \ + postImage(session,attachImageFilename,[],inboxUrl,headers,"inbox:write") + #if not postResult: + # if debug: + # print('DEBUG: Failed to upload image') + # return 9 + 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 not postResult: + # if debug: + # print('DEBUG: POST failed for c2s to '+inboxUrl) + # return 5 if debug: print('DEBUG: c2s POST success') diff --git a/session.py b/session.py index dab3bc7b..da3a9844 100644 --- a/session.py +++ b/session.py @@ -6,6 +6,7 @@ __maintainer__ = "Bob Mottram" __email__ = "bob@freedombone.net" __status__ = "Production" +import os import requests from utils import urlPermitted import json @@ -55,3 +56,36 @@ def postJson(session,postJsonObject: {},federationList: [],inboxUrl: str,headers postResult = session.post(url = inboxUrl, data = json.dumps(postJsonObject), headers=headers) return postResult.text + +def postImage(session,attachImageFilename: str,federationList: [],inboxUrl: str,headers: {},capability: str) -> str: + """Post an image to the inbox of another person or outbox via c2s + Supplying a capability, such as "inbox:write" + """ + # always allow capability requests + if not capability.startswith('cap'): + # check that we are posting to a permitted domain + if not urlPermitted(inboxUrl,federationList,capability): + print('postJson: '+inboxUrl+' not permitted') + return None + + if not (attachImageFilename.endswith('.jpg') or \ + attachImageFilename.endswith('.jpeg') or \ + attachImageFilename.endswith('.png') or \ + attachImageFilename.endswith('.gif')): + print('Image must be png, jpg, or gif') + return None + if not os.path.isfile(attachImageFilename): + print('Image not found: '+attachImageFilename) + return None + contentType='image/jpeg' + if attachImageFilename.endswith('.png'): + contentType='image/png' + if attachImageFilename.endswith('.gif'): + contentType='image/gif' + headers['Content-type']=contentType + + with open(attachImageFilename, 'rb') as avFile: + mediaBinary = avFile.read() + postResult = session.post(url=inboxUrl, data=mediaBinary, headers=headers) + return postResult.text + return None diff --git a/tests.py b/tests.py index 26f22425..c5279bea 100644 --- a/tests.py +++ b/tests.py @@ -1015,8 +1015,8 @@ def testClientToServer(): sessionAlice = createSession(aliceDomain,alicePort,useTor) followersOnly=False - attachImageFilename=None - imageDescription=None + attachedImageFilename=baseDir+'/img/logo.png' + attachedImageDescription='Logo' useBlurhash=False cachedWebfingers={} personCache={} @@ -1030,7 +1030,7 @@ def testClientToServer(): aliceDomain,alicePort, \ 'bob',bobDomain,bobPort,None, \ httpPrefix,'Sent from my ActivityPub client',followersOnly, \ - attachImageFilename,imageDescription,useBlurhash, \ + attachedImageFilename,attachedImageDescription,useBlurhash, \ cachedWebfingers,personCache, \ True,None,None,None) print('sendResult: '+str(sendResult)) @@ -1064,7 +1064,7 @@ def testClientToServer(): assert thrBob.isAlive()==False os.chdir(baseDir) - shutil.rmtree(aliceDir) + #shutil.rmtree(aliceDir) shutil.rmtree(bobDir) def runAllTests():