epicyon/daemon.py

882 lines
42 KiB
Python
Raw Normal View History

2019-06-28 18:55:29 +00:00
__filename__ = "daemon.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "0.0.1"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer
2019-06-28 18:55:29 +00:00
#import socketserver
2019-07-06 21:24:47 +00:00
import commentjson
2019-06-28 18:55:29 +00:00
import json
2019-07-01 14:30:48 +00:00
import time
2019-06-28 18:55:29 +00:00
from pprint import pprint
from session import createSession
from webfinger import webfingerMeta
from webfinger import webfingerLookup
2019-07-01 21:01:43 +00:00
from webfinger import webfingerHandle
2019-06-28 18:55:29 +00:00
from person import personLookup
2019-07-04 16:24:23 +00:00
from person import personBoxJson
2019-07-11 12:29:31 +00:00
from person import createSharedInbox
from posts import outboxMessageCreateWrap
2019-07-04 16:24:23 +00:00
from posts import savePostToBox
2019-07-15 17:22:51 +00:00
from posts import sendToFollowers
from posts import postIsAddressedToPublic
2019-07-15 18:20:52 +00:00
from posts import sendToNamedAddresses
2019-06-28 21:59:54 +00:00
from inbox import inboxPermittedMessage
2019-07-02 15:07:27 +00:00
from inbox import inboxMessageHasParams
2019-07-04 12:23:53 +00:00
from inbox import runInboxQueue
2019-07-04 14:36:29 +00:00
from inbox import savePostToInboxQueue
2019-06-29 20:21:37 +00:00
from follow import getFollowingFeed
2019-07-17 10:34:00 +00:00
from follow import outboxUndoFollow
2019-07-03 18:24:44 +00:00
from auth import authorize
2019-07-16 16:08:21 +00:00
from auth import createPassword
2019-07-04 12:23:53 +00:00
from threads import threadWithTrace
2019-07-16 16:08:21 +00:00
from media import getMediaPath
from media import createMediaDirs
2019-07-17 17:16:48 +00:00
from delete import outboxDelete
2019-06-28 18:55:29 +00:00
import os
2019-06-28 21:06:05 +00:00
import sys
2019-06-28 18:55:29 +00:00
2019-06-29 14:35:26 +00:00
# maximum number of posts to list in outbox feed
maxPostsInFeed=20
2019-06-29 20:21:37 +00:00
# number of follows/followers per page
2019-06-29 20:34:41 +00:00
followsPerPage=12
2019-06-29 20:21:37 +00:00
2019-06-28 18:55:29 +00:00
def readFollowList(filename: str):
"""Returns a list of ActivityPub addresses to follow
"""
followlist=[]
if not os.path.isfile(filename):
return followlist
followUsers = open(filename, "r")
for u in followUsers:
if u not in followlist:
2019-07-03 09:40:27 +00:00
nickname,domain = parseHandle(u)
if nickname:
followlist.append(nickname+'@'+domain)
2019-06-29 20:21:37 +00:00
followUsers.close()
2019-06-28 18:55:29 +00:00
return followlist
class PubServer(BaseHTTPRequestHandler):
def _set_headers(self,fileFormat: str) -> None:
2019-06-28 18:55:29 +00:00
self.send_response(200)
self.send_header('Content-type', fileFormat)
self.end_headers()
def _404(self) -> None:
2019-06-28 18:55:29 +00:00
self.send_response(404)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.end_headers()
self.wfile.write("<html><head></head><body><h1>404 Not Found</h1></body></html>".encode('utf-8'))
def _webfinger(self) -> bool:
if not self.path.startswith('/.well-known'):
return False
2019-07-04 14:36:29 +00:00
if self.server.debug:
print('DEBUG: WEBFINGER well-known')
2019-06-28 18:55:29 +00:00
2019-07-03 16:14:45 +00:00
if self.server.debug:
print('DEBUG: WEBFINGER host-meta')
2019-06-28 18:55:29 +00:00
if self.path.startswith('/.well-known/host-meta'):
wfResult=webfingerMeta()
if wfResult:
self._set_headers('application/xrd+xml')
self.wfile.write(wfResult.encode('utf-8'))
return
2019-07-03 16:14:45 +00:00
if self.server.debug:
print('DEBUG: WEBFINGER lookup '+self.path+' '+str(self.server.baseDir))
2019-06-30 22:56:37 +00:00
wfResult=webfingerLookup(self.path,self.server.baseDir)
2019-06-28 18:55:29 +00:00
if wfResult:
2019-07-01 21:01:43 +00:00
self._set_headers('application/jrd+json')
2019-06-28 18:55:29 +00:00
self.wfile.write(json.dumps(wfResult).encode('utf-8'))
else:
2019-07-03 16:14:45 +00:00
if self.server.debug:
print('DEBUG: WEBFINGER lookup 404 '+self.path)
2019-06-28 18:55:29 +00:00
self._404()
return True
def _permittedDir(self,path: str) -> bool:
"""These are special paths which should not be accessible
directly via GET or POST
"""
2019-06-28 18:55:29 +00:00
if path.startswith('/wfendpoints') or \
path.startswith('/keys') or \
path.startswith('/accounts'):
return False
return True
2019-07-05 11:41:09 +00:00
def _postToOutbox(self,messageJson: {}) -> bool:
"""post is received by the outbox
Client to server message post
https://www.w3.org/TR/activitypub/#client-to-server-outbox-delivery
"""
2019-07-03 22:59:56 +00:00
if not messageJson.get('type'):
2019-07-04 10:02:56 +00:00
if self.server.debug:
print('DEBUG: POST to outbox has no "type" parameter')
2019-07-03 22:59:56 +00:00
return False
if not messageJson.get('object') and messageJson.get('content'):
if messageJson['type']!='Create':
# https://www.w3.org/TR/activitypub/#object-without-create
if self.server.debug:
2019-07-04 10:02:56 +00:00
print('DEBUG: POST to outbox - adding Create wrapper')
2019-07-03 22:59:56 +00:00
messageJson= \
outboxMessageCreateWrap(self.server.httpPrefix, \
self.postToNickname, \
2019-07-16 10:19:04 +00:00
self.server.domain, \
self.server.port, \
messageJson)
2019-07-03 22:44:03 +00:00
if messageJson['type']=='Create':
if not (messageJson.get('id') and \
messageJson.get('type') and \
messageJson.get('actor') and \
messageJson.get('object') and \
messageJson.get('to')):
if self.server.debug:
2019-07-16 10:19:04 +00:00
pprint(messageJson)
2019-07-04 10:02:56 +00:00
print('DEBUG: POST to outbox - Create does not have the required parameters')
2019-07-03 22:44:03 +00:00
return False
# https://www.w3.org/TR/activitypub/#create-activity-outbox
messageJson['object']['attributedTo']=messageJson['actor']
2019-07-16 16:08:21 +00:00
if messageJson['object'].get('attachment'):
attachmentIndex=0
if messageJson['object']['attachment'][attachmentIndex].get('mediaType'):
fileExtension='png'
if messageJson['object']['attachment'][attachmentIndex]['mediaType'].endswith('jpeg'):
fileExtension='jpg'
if messageJson['object']['attachment'][attachmentIndex]['mediaType'].endswith('gif'):
fileExtension='gif'
mediaDir=self.server.baseDir+'/accounts/'+self.postToNickname+'@'+self.server.domain
uploadMediaFilename=mediaDir+'/upload.'+fileExtension
if not os.path.isfile(uploadMediaFilename):
del messageJson['object']['attachment']
else:
# generate a path for the uploaded image
mPath=getMediaPath()
mediaPath=mPath+'/'+createPassword(32)+'.'+fileExtension
createMediaDirs(self.server.baseDir,mPath)
mediaFilename=self.server.baseDir+'/'+mediaPath
# move the uploaded image to its new path
os.rename(uploadMediaFilename,mediaFilename)
# change the url of the attachment
messageJson['object']['attachment'][attachmentIndex]['url']= \
self.server.httpPrefix+'://'+self.server.domain+'/'+mediaPath
2019-07-06 17:00:22 +00:00
permittedOutboxTypes=[
'Create','Announce','Like','Follow','Undo', \
'Update','Add','Remove','Block','Delete'
]
2019-07-03 22:44:03 +00:00
if messageJson['type'] not in permittedOutboxTypes:
if self.server.debug:
2019-07-06 17:00:22 +00:00
print('DEBUG: POST to outbox - '+messageJson['type']+ \
' is not a permitted activity type')
return False
2019-07-03 22:59:56 +00:00
if messageJson.get('id'):
2019-07-16 21:38:06 +00:00
postId=messageJson['id'].replace('/activity','')
2019-07-03 22:59:56 +00:00
else:
postId=None
2019-07-16 10:19:04 +00:00
if self.server.debug:
pprint(messageJson)
2019-07-17 17:16:48 +00:00
print('DEBUG: savePostToBox')
2019-07-16 10:19:04 +00:00
savePostToBox(self.server.baseDir, \
self.server.httpPrefix, \
postId, \
2019-07-15 18:20:52 +00:00
self.postToNickname, \
self.server.domain,messageJson,'outbox')
2019-07-16 10:19:04 +00:00
if not self.server.session:
2019-07-16 10:20:03 +00:00
if self.server.debug:
print('DEBUG: creating new session for c2s')
2019-07-16 10:19:04 +00:00
self.server.session= \
createSession(self.server.domain,self.server.port,self.server.useTor)
2019-07-15 17:22:51 +00:00
if self.server.debug:
print('DEBUG: sending c2s post to followers')
sendToFollowers(self.server.session,self.server.baseDir, \
2019-07-16 10:19:04 +00:00
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)
2019-07-17 10:34:00 +00:00
if self.server.debug:
print('DEBUG: handle any unfollow requests')
outboxUndoFollow(self.server.baseDir,messageJson,self.server.debug)
2019-07-17 17:58:08 +00:00
if self.server.debug:
print('DEBUG: handle delete requests')
outboxDelete(self.server.baseDir,self.server.httpPrefix, \
self.postToNickname,self.server.domain, \
messageJson,self.server.debug)
2019-07-15 18:20:52 +00:00
if self.server.debug:
print('DEBUG: sending c2s post to named addresses')
2019-07-16 10:19:04 +00:00
print('c2s sender: '+self.postToNickname+'@'+self.server.domain+':'+str(self.server.port))
2019-07-15 18:20:52 +00:00
sendToNamedAddresses(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)
return True
2019-07-15 12:27:26 +00:00
def _updateInboxQueue(self,nickname: str,messageJson: {}) -> int:
2019-07-05 11:27:18 +00:00
"""Update the inbox queue
"""
2019-07-15 12:27:26 +00:00
# Check if the queue is full
if len(self.server.inboxQueue)>=self.server.maxQueueLength:
return 1
# save the json for later queue processing
2019-07-06 13:49:25 +00:00
queueFilename = \
2019-07-05 11:27:18 +00:00
savePostToInboxQueue(self.server.baseDir, \
self.server.httpPrefix, \
nickname, \
self.server.domain, \
messageJson,
self.headers['host'],
2019-07-05 22:13:20 +00:00
self.headers['signature'],
2019-07-06 13:49:25 +00:00
'/'+self.path.split('/')[-1],
self.server.debug)
if queueFilename:
2019-07-15 12:27:26 +00:00
# add json to the queue
2019-07-06 13:49:25 +00:00
if queueFilename not in self.server.inboxQueue:
self.server.inboxQueue.append(queueFilename)
2019-07-05 11:27:18 +00:00
self.send_response(201)
self.end_headers()
self.server.POSTbusy=False
2019-07-15 12:27:26 +00:00
return 0
return 2
2019-07-05 11:27:18 +00:00
2019-07-12 11:05:43 +00:00
def _isAuthorized(self) -> bool:
if self.headers.get('Authorization'):
if authorize(self.server.baseDir,self.path, \
self.headers['Authorization'], \
self.server.debug):
return True
return False
2019-07-01 21:01:43 +00:00
def do_GET(self):
2019-07-03 16:14:45 +00:00
if self.server.debug:
2019-07-06 17:00:22 +00:00
print('DEBUG: GET from '+self.server.baseDir+ \
' path: '+self.path+' busy: '+ \
str(self.server.GETbusy))
2019-07-01 14:30:48 +00:00
if self.server.GETbusy:
currTimeGET=int(time.time())
if currTimeGET-self.server.lastGET<10:
2019-07-03 16:14:45 +00:00
if self.server.debug:
print('DEBUG: GET Busy')
2019-06-29 17:28:43 +00:00
self.send_response(429)
2019-06-29 17:25:09 +00:00
self.end_headers()
2019-07-01 14:30:48 +00:00
return
self.server.lastGET=currTimeGET
self.server.GETbusy=True
2019-06-30 20:03:23 +00:00
2019-06-28 21:59:54 +00:00
if not self._permittedDir(self.path):
2019-07-03 16:14:45 +00:00
if self.server.debug:
print('DEBUG: GET Not permitted')
2019-06-28 18:55:29 +00:00
self._404()
2019-07-01 14:30:48 +00:00
self.server.GETbusy=False
2019-06-28 18:55:29 +00:00
return
# get webfinger endpoint for a person
if self._webfinger():
2019-07-01 14:30:48 +00:00
self.server.GETbusy=False
2019-06-28 18:55:29 +00:00
return
2019-07-12 19:33:34 +00:00
# show media
if '/media/' in self.path:
if self.path.endswith('.png') or \
self.path.endswith('.jpg') or \
self.path.endswith('.gif'):
mediaStr=self.path.split('/media/')[1]
mediaFilename= \
self.server.baseDir+'/media/'+mediaStr
if os.path.isfile(mediaFilename):
if mediaFilename.endswith('.png'):
self._set_headers('image/png')
elif mediaFilename.endswith('.jpg'):
self._set_headers('image/jpeg')
else:
self._set_headers('image/gif')
with open(mediaFilename, 'rb') as avFile:
mediaBinary = avFile.read()
self.wfile.write(mediaBinary)
self.server.GETbusy=False
return
2019-07-16 16:10:52 +00:00
self._404()
self.server.GETbusy=False
return
2019-07-12 16:09:25 +00:00
# show avatar or background image
2019-07-12 16:03:01 +00:00
if '/users/' in self.path:
if self.path.endswith('.png') or \
self.path.endswith('.jpg') or \
self.path.endswith('.gif'):
avatarStr=self.path.split('/users/')[1]
if '/' in avatarStr:
avatarNickname=avatarStr.split('/')[0]
avatarFile=avatarStr.split('/')[1]
avatarFilename= \
self.server.baseDir+'/accounts/'+ \
avatarNickname+'@'+ \
self.server.domain+'/'+avatarFile
if os.path.isfile(avatarFilename):
if avatarFile.endswith('.png'):
self._set_headers('image/png')
elif avatarFile.endswith('.jpg'):
self._set_headers('image/jpeg')
else:
self._set_headers('image/gif')
with open(avatarFilename, 'rb') as avFile:
2019-07-12 19:33:34 +00:00
avBinary = avFile.read()
self.wfile.write(avBinary)
2019-07-12 16:03:01 +00:00
self.server.GETbusy=False
return
2019-07-06 21:33:46 +00:00
# get an individual post from the path /@nickname/statusnumber
if '/@' in self.path:
namedStatus=self.path.split('/@')[1]
if '/' in namedStatus:
postSections=namedStatus.split('/')
if len(postSections)==2:
nickname=postSections[0]
statusNumber=postSections[1]
if len(statusNumber)>10 and statusNumber.isdigit():
domainFull=self.server.domain
if self.server.port!=80 and self.server.port!=443:
domainFull=self.server.domain+':'+str(self.server.port)
postFilename= \
self.server.baseDir+'/accounts/'+nickname+'@'+self.server.domain+'/outbox/'+ \
self.server.httpPrefix+':##'+domainFull+'#users#'+nickname+'#statuses#'+statusNumber+'.json'
if os.path.isfile(postFilename):
2019-07-14 16:57:06 +00:00
postJsonObject={}
2019-07-06 21:33:46 +00:00
with open(postFilename, 'r') as fp:
2019-07-14 16:57:06 +00:00
postJsonObject=commentjson.load(fp)
# Only authorized viewers get to see likes on posts
# Otherwize marketers could gain more social graph info
if not self._isAuthorized():
2019-07-14 16:57:06 +00:00
if postJsonObject.get('likes'):
postJsonObject['likes']={}
self._set_headers('application/json')
2019-07-14 16:57:06 +00:00
self.wfile.write(json.dumps(postJsonObject).encode('utf-8'))
2019-07-06 21:33:46 +00:00
self.server.GETbusy=False
return
else:
self._404()
self.server.GETbusy=False
2019-07-13 19:28:14 +00:00
return
# get replies to a post /users/nickname/statuses/number/replies
2019-07-13 20:23:42 +00:00
if self.path.endswith('/replies') or '/replies?page=' in self.path:
2019-07-13 19:28:14 +00:00
if '/statuses/' in self.path and '/users/' in self.path:
namedStatus=self.path.split('/users/')[1]
if '/' in namedStatus:
postSections=namedStatus.split('/')
if len(postSections)>=4:
if postSections[3].startswith('replies'):
nickname=postSections[0]
statusNumber=postSections[2]
if len(statusNumber)>10 and statusNumber.isdigit():
#get the replies file
domainFull=self.server.domain
if self.server.port!=80 and self.server.port!=443:
domainFull=self.server.domain+':'+str(self.server.port)
2019-07-13 19:34:03 +00:00
boxname='outbox'
postDir=self.server.baseDir+'/accounts/'+nickname+'@'+self.server.domain+'/'+boxname
postRepliesFilename= \
postDir+'/'+ \
self.server.httpPrefix+':##'+domainFull+'#users#'+nickname+'#statuses#'+statusNumber+'.replies'
if not os.path.isfile(postRepliesFilename):
# There are no replies, so show empty collection
repliesJson = {
'@context': 'https://www.w3.org/ns/activitystreams',
2019-07-13 20:23:42 +00:00
'first': self.server.httpPrefix+'://'+domainFull+'/users/'+nickname+'/statuses/'+statusNumber+'/replies?page=true',
'id': self.server.httpPrefix+'://'+domainFull+'/users/'+nickname+'/statuses/'+statusNumber+'/replies',
'last': self.server.httpPrefix+'://'+domainFull+'/users/'+nickname+'/statuses/'+statusNumber+'/replies?page=true',
2019-07-13 19:34:03 +00:00
'totalItems': 0,
'type': 'OrderedCollection'}
self._set_headers('application/json')
self.wfile.write(json.dumps(repliesJson).encode('utf-8'))
self.server.GETbusy=False
return
else:
# replies exist. Itterate through the text file containing message ids
repliesJson = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': self.server.httpPrefix+'://'+domainFull+'/users/'+nickname+'/statuses/'+statusNumber+'?page=true',
'orderedItems': [
],
'partOf': self.server.httpPrefix+'://'+domainFull+'/users/'+nickname+'/statuses/'+statusNumber,
'type': 'OrderedCollectionPage'}
# some messages could be private, so check authorization state
authorized=self._isAuthorized()
# populate the items list with replies
repliesBoxes=['outbox','inbox']
with open(postRepliesFilename,'r') as repliesFile:
for messageId in repliesFile:
replyFound=False
# examine inbox and outbox
for boxname in repliesBoxes:
searchFilename= \
self.server.baseDir+ \
'/accounts/'+nickname+'@'+ \
self.server.domain+'/'+ \
boxname+'/'+ \
messageId.replace('\n','').replace('/','#')+'.json'
if os.path.isfile(searchFilename):
if authorized or \
'https://www.w3.org/ns/activitystreams#Public' in open(searchFilename).read():
with open(searchFilename, 'r') as fp:
2019-07-14 16:57:06 +00:00
postJsonObject=commentjson.load(fp)
if postJsonObject['object'].get('cc'):
if authorized or \
2019-07-14 16:57:06 +00:00
('https://www.w3.org/ns/activitystreams#Public' in postJsonObject['object']['to'] or \
'https://www.w3.org/ns/activitystreams#Public' in postJsonObject['object']['cc']):
repliesJson['orderedItems'].append(postJsonObject)
replyFound=True
else:
if authorized or \
2019-07-14 16:57:06 +00:00
'https://www.w3.org/ns/activitystreams#Public' in postJsonObject['object']['to']:
repliesJson['orderedItems'].append(postJsonObject)
replyFound=True
2019-07-13 19:34:03 +00:00
break
# if not in either inbox or outbox then examine the shared inbox
if not replyFound:
searchFilename= \
self.server.baseDir+ \
'/accounts/inbox@'+ \
self.server.domain+'/inbox/'+ \
messageId.replace('\n','').replace('/','#')+'.json'
if os.path.isfile(searchFilename):
if authorized or \
'https://www.w3.org/ns/activitystreams#Public' in open(searchFilename).read():
# get the json of the reply and append it to the collection
with open(searchFilename, 'r') as fp:
2019-07-14 16:57:06 +00:00
postJsonObject=commentjson.load(fp)
if postJsonObject['object'].get('cc'):
if authorized or \
2019-07-14 16:57:06 +00:00
('https://www.w3.org/ns/activitystreams#Public' in postJsonObject['object']['to'] or \
'https://www.w3.org/ns/activitystreams#Public' in postJsonObject['object']['cc']):
repliesJson['orderedItems'].append(postJsonObject)
else:
if authorized or \
2019-07-14 16:57:06 +00:00
'https://www.w3.org/ns/activitystreams#Public' in postJsonObject['object']['to']:
repliesJson['orderedItems'].append(postJsonObject)
2019-07-13 19:34:03 +00:00
# send the replies json
self._set_headers('application/json')
self.wfile.write(json.dumps(repliesJson).encode('utf-8'))
self.server.GETbusy=False
return
2019-07-13 19:28:14 +00:00
2019-07-06 21:33:46 +00:00
# get an individual post from the path /users/nickname/statuses/number
2019-07-06 21:24:47 +00:00
if '/statuses/' in self.path and '/users/' in self.path:
namedStatus=self.path.split('/users/')[1]
if '/' in namedStatus:
postSections=namedStatus.split('/')
2019-07-13 19:28:14 +00:00
if len(postSections)>=3:
2019-07-06 21:24:47 +00:00
nickname=postSections[0]
statusNumber=postSections[2]
2019-07-06 21:33:46 +00:00
if len(statusNumber)>10 and statusNumber.isdigit():
2019-07-06 21:24:47 +00:00
domainFull=self.server.domain
if self.server.port!=80 and self.server.port!=443:
domainFull=self.server.domain+':'+str(self.server.port)
postFilename= \
self.server.baseDir+'/accounts/'+nickname+'@'+self.server.domain+'/outbox/'+ \
self.server.httpPrefix+':##'+domainFull+'#users#'+nickname+'#statuses#'+statusNumber+'.json'
if os.path.isfile(postFilename):
2019-07-14 16:57:06 +00:00
postJsonObject={}
2019-07-06 21:24:47 +00:00
with open(postFilename, 'r') as fp:
2019-07-14 16:57:06 +00:00
postJsonObject=commentjson.load(fp)
# Only authorized viewers get to see likes on posts
# Otherwize marketers could gain more social graph info
if not self._isAuthorized():
2019-07-14 16:57:06 +00:00
if postJsonObject.get('likes'):
postJsonObject['likes']={}
self._set_headers('application/json')
2019-07-14 16:57:06 +00:00
self.wfile.write(json.dumps(postJsonObject).encode('utf-8'))
2019-07-06 21:24:47 +00:00
self.server.GETbusy=False
return
else:
self._404()
self.server.GETbusy=False
return
2019-07-03 19:32:07 +00:00
# get the inbox for a given person
if self.path.endswith('/inbox'):
if '/users/' in self.path:
2019-07-12 11:05:43 +00:00
if self._isAuthorized():
inboxFeed=personBoxJson(self.server.baseDir, \
self.server.domain, \
self.server.port, \
self.path, \
self.server.httpPrefix, \
maxPostsInFeed, 'inbox', \
True,self.server.ocapAlways)
2019-07-12 11:05:43 +00:00
if inboxFeed:
self._set_headers('application/json')
self.wfile.write(json.dumps(inboxFeed).encode('utf-8'))
self.server.GETbusy=False
return
else:
if self.server.debug:
print('DEBUG: '+nickname+ \
' was not authorized to access '+self.path)
2019-07-04 08:56:15 +00:00
if self.server.debug:
print('DEBUG: GET access to inbox is unauthorized')
2019-07-03 20:32:30 +00:00
self.send_response(405)
self.end_headers()
self.server.POSTbusy=False
return
2019-07-03 19:32:07 +00:00
2019-06-29 14:35:26 +00:00
# get outbox feed for a person
2019-07-04 16:24:23 +00:00
outboxFeed=personBoxJson(self.server.baseDir,self.server.domain, \
self.server.port,self.path, \
self.server.httpPrefix, \
maxPostsInFeed, 'outbox', \
self._isAuthorized(), \
self.server.ocapAlways)
2019-06-29 14:41:23 +00:00
if outboxFeed:
2019-06-29 14:35:26 +00:00
self._set_headers('application/json')
2019-06-29 14:41:23 +00:00
self.wfile.write(json.dumps(outboxFeed).encode('utf-8'))
2019-07-01 14:30:48 +00:00
self.server.GETbusy=False
2019-06-29 20:21:37 +00:00
return
2019-07-02 20:54:22 +00:00
following=getFollowingFeed(self.server.baseDir,self.server.domain, \
self.server.port,self.path, \
2019-07-03 19:00:03 +00:00
self.server.httpPrefix,followsPerPage)
2019-06-29 20:21:37 +00:00
if following:
self._set_headers('application/json')
self.wfile.write(json.dumps(following).encode('utf-8'))
2019-07-01 14:30:48 +00:00
self.server.GETbusy=False
2019-06-29 20:21:37 +00:00
return
2019-07-02 20:54:22 +00:00
followers=getFollowingFeed(self.server.baseDir,self.server.domain, \
self.server.port,self.path, \
2019-07-06 17:00:22 +00:00
self.server.httpPrefix, \
followsPerPage,'followers')
2019-06-29 20:21:37 +00:00
if followers:
self._set_headers('application/json')
self.wfile.write(json.dumps(followers).encode('utf-8'))
2019-07-01 14:30:48 +00:00
self.server.GETbusy=False
2019-07-04 14:36:29 +00:00
return
2019-06-28 18:55:29 +00:00
# look up a person
2019-07-02 20:54:22 +00:00
getPerson = personLookup(self.server.domain,self.path, \
self.server.baseDir)
2019-06-28 18:55:29 +00:00
if getPerson:
self._set_headers('application/json')
self.wfile.write(json.dumps(getPerson).encode('utf-8'))
2019-07-01 14:30:48 +00:00
self.server.GETbusy=False
2019-06-28 18:55:29 +00:00
return
# check that a json file was requested
if not self.path.endswith('.json'):
2019-07-03 16:14:45 +00:00
if self.server.debug:
print('DEBUG: GET Not json: '+self.path+' '+self.server.baseDir)
2019-06-28 18:55:29 +00:00
self._404()
2019-07-01 14:30:48 +00:00
self.server.GETbusy=False
2019-06-28 18:55:29 +00:00
return
# check that the file exists
2019-06-30 22:56:37 +00:00
filename=self.server.baseDir+self.path
2019-06-28 18:55:29 +00:00
if os.path.isfile(filename):
self._set_headers('application/json')
2019-07-03 19:15:42 +00:00
with open(filename, 'r', encoding='utf-8') as File:
2019-06-28 18:55:29 +00:00
content = File.read()
contentJson=json.loads(content)
2019-07-03 19:15:42 +00:00
self.wfile.write(json.dumps(contentJson).encode('utf-8'))
2019-06-28 18:55:29 +00:00
else:
2019-07-03 16:14:45 +00:00
if self.server.debug:
print('DEBUG: GET Unknown file')
2019-06-28 18:55:29 +00:00
self._404()
2019-07-01 14:30:48 +00:00
self.server.GETbusy=False
2019-06-28 18:55:29 +00:00
def do_HEAD(self):
self._set_headers('application/json')
2019-07-01 21:01:43 +00:00
2019-06-28 18:55:29 +00:00
def do_POST(self):
2019-07-04 17:56:25 +00:00
if self.server.debug:
2019-07-06 17:00:22 +00:00
print('DEBUG: POST to from '+self.server.baseDir+ \
' path: '+self.path+' busy: '+ \
str(self.server.POSTbusy))
2019-07-01 14:30:48 +00:00
if self.server.POSTbusy:
currTimePOST=int(time.time())
if currTimePOST-self.server.lastPOST<10:
2019-06-29 17:28:43 +00:00
self.send_response(429)
2019-06-29 17:27:32 +00:00
self.end_headers()
2019-07-01 14:30:48 +00:00
return
self.server.lastPOST=currTimePOST
2019-07-03 16:14:45 +00:00
2019-07-01 14:30:48 +00:00
self.server.POSTbusy=True
2019-07-01 11:48:54 +00:00
if not self.headers.get('Content-type'):
2019-07-03 16:14:45 +00:00
print('Content-type header missing')
2019-07-01 11:48:54 +00:00
self.send_response(400)
self.end_headers()
2019-07-01 14:30:48 +00:00
self.server.POSTbusy=False
2019-07-01 11:48:54 +00:00
return
2019-07-03 16:14:45 +00:00
2019-07-03 21:37:46 +00:00
# remove any trailing slashes from the path
2019-07-05 10:21:10 +00:00
self.path=self.path.replace('/outbox/','/outbox').replace('/inbox/','/inbox').replace('/sharedInbox/','/sharedInbox')
2019-07-03 21:37:46 +00:00
# if this is a POST to teh outbox then check authentication
self.outboxAuthenticated=False
self.postToNickname=None
2019-07-04 10:02:56 +00:00
if self.path.endswith('/outbox'):
if '/users/' in self.path:
2019-07-12 11:05:43 +00:00
if self._isAuthorized():
self.outboxAuthenticated=True
2019-07-16 10:19:04 +00:00
pathUsersSection=self.path.split('/users/')[1]
2019-07-12 11:05:43 +00:00
self.postToNickname=pathUsersSection.split('/')[0]
2019-07-03 21:37:46 +00:00
if not self.outboxAuthenticated:
self.send_response(405)
self.end_headers()
self.server.POSTbusy=False
return
2019-07-03 16:14:45 +00:00
# check that the post is to an expected path
2019-07-05 10:21:10 +00:00
if not (self.path.endswith('/outbox') or \
self.path.endswith('/inbox') or \
self.path.endswith('/caps/new') or \
2019-07-05 10:21:10 +00:00
self.path=='/sharedInbox'):
2019-07-03 16:14:45 +00:00
print('Attempt to POST to invalid path '+self.path)
self.send_response(400)
self.end_headers()
self.server.POSTbusy=False
return
2019-06-28 18:55:29 +00:00
# read the message and convert it into a python dictionary
2019-07-01 11:48:54 +00:00
length = int(self.headers['Content-length'])
2019-07-03 16:14:45 +00:00
if self.server.debug:
print('DEBUG: content-length: '+str(length))
2019-07-16 14:23:06 +00:00
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'])
2019-06-28 21:06:05 +00:00
self.send_response(400)
self.end_headers()
2019-07-01 14:30:48 +00:00
self.server.POSTbusy=False
2019-06-28 21:06:05 +00:00
return
2019-07-03 16:14:45 +00:00
if self.server.debug:
print('DEBUG: Reading message')
2019-07-01 11:48:54 +00:00
messageBytes=self.rfile.read(length)
2019-07-16 14:23:06 +00:00
messageJson=json.loads(messageBytes)
2019-06-28 20:35:34 +00:00
2019-07-03 21:37:46 +00:00
# https://www.w3.org/TR/activitypub/#object-without-create
if self.outboxAuthenticated:
2019-07-16 10:19:04 +00:00
if self._postToOutbox(messageJson):
2019-07-16 19:07:45 +00:00
if messageJson.get('id'):
2019-07-16 10:19:04 +00:00
self.headers['Location']= \
2019-07-16 19:07:45 +00:00
messageJson['id'].replace('/activity','')
self.send_response(201)
self.end_headers()
self.server.POSTbusy=False
return
else:
self.send_response(403)
self.end_headers()
self.server.POSTbusy=False
return
2019-07-03 21:37:46 +00:00
2019-07-02 15:07:27 +00:00
# check the necessary properties are available
2019-07-03 16:14:45 +00:00
if self.server.debug:
print('DEBUG: Check message has params')
2019-07-05 15:10:21 +00:00
if self.path.endswith('/inbox') or \
self.path=='/sharedInbox':
2019-07-03 21:37:46 +00:00
if not inboxMessageHasParams(messageJson):
2019-07-06 13:49:25 +00:00
if self.server.debug:
2019-07-16 10:19:04 +00:00
pprint(messageJson)
2019-07-06 13:49:25 +00:00
print("DEBUG: inbox message doesn't have the required parameters")
2019-07-03 21:37:46 +00:00
self.send_response(403)
self.end_headers()
self.server.POSTbusy=False
return
2019-07-02 15:07:27 +00:00
2019-07-06 17:00:22 +00:00
if not inboxPermittedMessage(self.server.domain, \
messageJson, \
2019-07-09 14:20:23 +00:00
self.server.federationList):
2019-07-03 16:14:45 +00:00
if self.server.debug:
# https://www.youtube.com/watch?v=K3PrSj9XEu4
2019-07-03 16:14:45 +00:00
print('DEBUG: Ah Ah Ah')
2019-06-28 21:59:54 +00:00
self.send_response(403)
self.end_headers()
2019-07-01 14:30:48 +00:00
self.server.POSTbusy=False
2019-07-01 11:48:54 +00:00
return
2019-07-01 14:30:48 +00:00
2019-07-05 10:21:10 +00:00
if self.server.debug:
pprint(messageJson)
2019-07-02 17:11:59 +00:00
2019-07-04 14:36:29 +00:00
if not self.headers.get('signature'):
if 'keyId=' not in self.headers['signature']:
if self.server.debug:
print('DEBUG: POST to inbox has no keyId in header signature parameter')
self.send_response(403)
self.end_headers()
self.server.POSTbusy=False
return
2019-07-04 10:02:56 +00:00
2019-07-03 16:14:45 +00:00
if self.server.debug:
2019-07-06 13:49:25 +00:00
print('DEBUG: POST saving to inbox queue')
2019-07-04 10:02:56 +00:00
if '/users/' in self.path:
pathUsersSection=self.path.split('/users/')[1]
if '/' not in pathUsersSection:
if self.server.debug:
print('DEBUG: This is not a users endpoint')
else:
self.postToNickname=pathUsersSection.split('/')[0]
if self.postToNickname:
2019-07-15 12:27:26 +00:00
queueStatus=self._updateInboxQueue(self.postToNickname,messageJson)
if queueStatus==0:
self.send_response(200)
self.end_headers()
self.server.POSTbusy=False
2019-07-04 12:23:53 +00:00
return
2019-07-15 12:27:26 +00:00
if queueStatus==1:
2019-07-15 12:28:41 +00:00
self.send_response(503)
2019-07-15 12:27:26 +00:00
self.end_headers()
self.server.POSTbusy=False
return
2019-07-04 12:23:53 +00:00
self.send_response(403)
2019-07-01 14:30:48 +00:00
self.end_headers()
self.server.POSTbusy=False
return
2019-07-04 12:23:53 +00:00
else:
2019-07-05 15:10:21 +00:00
if self.path == '/sharedInbox' or self.path == '/inbox':
2019-07-05 10:24:20 +00:00
print('DEBUG: POST to shared inbox')
2019-07-15 12:27:26 +00:00
queueStatus-_updateInboxQueue('inbox',messageJson)
if queueStatus==0:
self.send_response(200)
self.end_headers()
self.server.POSTbusy=False
2019-07-05 11:27:18 +00:00
return
2019-07-15 12:27:26 +00:00
if queueStatus==1:
2019-07-15 12:28:41 +00:00
self.send_response(503)
2019-07-15 12:27:26 +00:00
self.end_headers()
self.server.POSTbusy=False
return
2019-07-05 10:24:20 +00:00
self.send_response(200)
self.end_headers()
self.server.POSTbusy=False
2019-07-03 16:14:45 +00:00
2019-07-13 09:37:17 +00:00
def runDaemon(clientToServer: bool,baseDir: str,domain: str, \
port=80,httpPrefix='https', \
2019-07-09 18:11:23 +00:00
fedList=[],noreply=False,nolike=False,nopics=False, \
noannounce=False,cw=False,ocapAlways=False, \
2019-07-15 10:22:19 +00:00
useTor=False,maxReplies=64, \
domainMaxPostsPerDay=8640,accountMaxPostsPerDay=8640, \
2019-07-17 17:44:26 +00:00
nodeletion=False,debug=False) -> None:
2019-06-28 18:55:29 +00:00
if len(domain)==0:
2019-07-03 09:24:55 +00:00
domain='localhost'
2019-06-28 18:55:29 +00:00
if '.' not in domain:
2019-07-03 12:24:54 +00:00
if domain != 'localhost':
print('Invalid domain: ' + domain)
return
2019-06-28 18:55:29 +00:00
2019-07-03 09:24:55 +00:00
serverAddress = ('', port)
2019-07-01 21:01:43 +00:00
httpd = ThreadingHTTPServer(serverAddress, PubServer)
2019-06-30 20:03:23 +00:00
httpd.domain=domain
httpd.port=port
2019-07-03 19:00:03 +00:00
httpd.httpPrefix=httpPrefix
2019-07-03 16:14:45 +00:00
httpd.debug=debug
2019-06-30 20:03:23 +00:00
httpd.federationList=fedList.copy()
2019-07-04 20:25:19 +00:00
httpd.baseDir=baseDir
2019-07-01 14:30:48 +00:00
httpd.personCache={}
httpd.cachedWebfingers={}
httpd.useTor=useTor
2019-07-02 17:11:59 +00:00
httpd.session = None
httpd.sessionLastUpdate=0
2019-07-01 14:30:48 +00:00
httpd.lastGET=0
httpd.lastPOST=0
httpd.GETbusy=False
httpd.POSTbusy=False
2019-07-01 21:01:43 +00:00
httpd.receivedMessage=False
2019-07-04 10:02:56 +00:00
httpd.inboxQueue=[]
2019-07-05 18:57:19 +00:00
httpd.sendThreads=[]
httpd.postLog=[]
2019-07-15 12:27:26 +00:00
httpd.maxQueueLength=16
httpd.ocapAlways=ocapAlways
2019-07-16 14:23:06 +00:00
httpd.maxMessageLength=5000
httpd.maxImageSize=10*1024*1024
2019-07-17 17:44:26 +00:00
httpd.nodeletion=nodeletion
2019-07-09 17:54:08 +00:00
httpd.acceptedCaps=["inbox:write","objects:read"]
if noreply:
httpd.acceptedCaps.append('inbox:noreply')
if nolike:
httpd.acceptedCaps.append('inbox:nolike')
2019-07-09 18:11:23 +00:00
if nopics:
httpd.acceptedCaps.append('inbox:nopics')
if noannounce:
httpd.acceptedCaps.append('inbox:noannounce')
if cw:
httpd.acceptedCaps.append('inbox:cw')
2019-07-11 12:29:31 +00:00
print('Creating shared inbox: inbox@'+domain)
createSharedInbox(baseDir,'inbox',domain,port,httpPrefix)
2019-07-12 09:52:06 +00:00
print('Creating inbox queue')
2019-07-06 17:00:22 +00:00
httpd.thrInboxQueue= \
threadWithTrace(target=runInboxQueue, \
args=(baseDir,httpPrefix,httpd.sendThreads, \
httpd.postLog,httpd.cachedWebfingers, \
httpd.personCache,httpd.inboxQueue, \
domain,port,useTor,httpd.federationList, \
2019-07-13 21:00:12 +00:00
httpd.ocapAlways,maxReplies, \
2019-07-15 10:22:19 +00:00
domainMaxPostsPerDay,accountMaxPostsPerDay, \
2019-07-17 17:44:26 +00:00
nodeletion,debug,httpd.acceptedCaps),daemon=True)
2019-07-04 12:23:53 +00:00
httpd.thrInboxQueue.start()
2019-07-13 09:37:17 +00:00
if clientToServer:
print('Running ActivityPub client on ' + domain + ' port ' + str(port))
else:
print('Running ActivityPub server on ' + domain + ' port ' + str(port))
2019-06-28 18:55:29 +00:00
httpd.serve_forever()