epicyon/daemon.py

310 lines
11 KiB
Python

__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
#import socketserver
import json
import time
from pprint import pprint
from session import createSession
from webfinger import webfingerMeta
from webfinger import webfingerLookup
from webfinger import webfingerHandle
from person import personLookup
from person import personKeyLookup
from person import personOutboxJson
from posts import getPersonPubKey
from inbox import inboxPermittedMessage
from inbox import inboxMessageHasParams
from follow import getFollowingFeed
import os
import sys
# Avoid giant messages
maxMessageLength=5000
# maximum number of posts to list in outbox feed
maxPostsInFeed=20
# number of follows/followers per page
followsPerPage=12
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:
username,domain = parseHandle(u)
if username:
followlist.append(username+'@'+domain)
followUsers.close()
return followlist
class PubServer(BaseHTTPRequestHandler):
def _set_headers(self,fileFormat):
self.send_response(200)
self.send_header('Content-type', fileFormat)
self.end_headers()
def _404(self):
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:
print('############### _webfinger well-known')
if not self.path.startswith('/.well-known'):
return False
print('############### _webfinger host-meta')
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
print('############### _webfinger lookup '+self.path+' '+str(self.server.baseDir))
wfResult=webfingerLookup(self.path,self.server.baseDir)
if wfResult:
self._set_headers('application/jrd+json')
self.wfile.write(json.dumps(wfResult).encode('utf-8'))
else:
print('############### _webfinger lookup 404 '+self.path)
self._404()
return True
def _permittedDir(self,path):
if path.startswith('/wfendpoints') or \
path.startswith('/keys') or \
path.startswith('/accounts'):
return False
return True
def do_GET(self):
print('############### GET from '+self.server.baseDir+' path: '+self.path)
if self.server.GETbusy:
currTimeGET=int(time.time())
if currTimeGET-self.server.lastGET<10:
print('############### Busy')
self.send_response(429)
self.end_headers()
return
self.server.lastGET=currTimeGET
self.server.GETbusy=True
print('############### _permittedDir')
if not self._permittedDir(self.path):
print('############# Not permitted')
self._404()
self.server.GETbusy=False
return
# get webfinger endpoint for a person
print('############### _webfinger')
if self._webfinger():
self.server.GETbusy=False
return
print('############### _webfinger end')
# get outbox feed for a person
outboxFeed=personOutboxJson(self.server.baseDir,self.server.domain, \
self.server.port,self.path, \
self.server.https,maxPostsInFeed)
if outboxFeed:
self._set_headers('application/json')
self.wfile.write(json.dumps(outboxFeed).encode('utf-8'))
self.server.GETbusy=False
return
following=getFollowingFeed(self.server.baseDir,self.server.domain, \
self.server.port,self.path, \
self.server.https,followsPerPage)
if following:
self._set_headers('application/json')
self.wfile.write(json.dumps(following).encode('utf-8'))
self.server.GETbusy=False
return
followers=getFollowingFeed(self.server.baseDir,self.server.domain, \
self.server.port,self.path, \
self.server.https,followsPerPage,'followers')
if followers:
self._set_headers('application/json')
self.wfile.write(json.dumps(followers).encode('utf-8'))
self.server.GETbusy=False
return
# look up a person
getPerson = personLookup(self.server.domain,self.path, \
self.server.baseDir)
if getPerson:
self._set_headers('application/json')
self.wfile.write(json.dumps(getPerson).encode('utf-8'))
self.server.GETbusy=False
return
personKey = personKeyLookup(self.server.domain,self.path, \
self.server.baseDir)
if personKey:
self._set_headers('text/html; charset=utf-8')
self.wfile.write(personKey.encode('utf-8'))
self.server.GETbusy=False
return
# check that a json file was requested
if not self.path.endswith('.json'):
print('############# Not json: '+self.path+' '+self.server.baseDir)
self._404()
self.server.GETbusy=False
return
# check that the file exists
filename=self.server.baseDir+self.path
if os.path.isfile(filename):
self._set_headers('application/json')
with open(filename, 'r', encoding='utf8') as File:
content = File.read()
contentJson=json.loads(content)
self.wfile.write(json.dumps(contentJson).encode('utf8'))
else:
print('############# Unknown file')
self._404()
self.server.GETbusy=False
def do_HEAD(self):
self._set_headers('application/json')
def do_POST(self):
if self.server.POSTbusy:
currTimePOST=int(time.time())
if currTimePOST-self.server.lastPOST<10:
self.send_response(429)
self.end_headers()
return
self.server.lastPOST=currTimePOST
print('**************** POST ready to receive')
self.server.POSTbusy=True
if not self.headers.get('Content-type'):
print('**************** No Content-type')
self.send_response(400)
self.end_headers()
self.server.POSTbusy=False
return
print('*****************headers: '+str(self.headers))
# refuse to receive non-json content
if self.headers['Content-type'] != 'application/json':
print("**************** That's no Json!")
self.send_response(400)
self.end_headers()
self.server.POSTbusy=False
return
# read the message and convert it into a python dictionary
length = int(self.headers['Content-length'])
print('**************** content-length: '+str(length))
if length>maxMessageLength:
self.send_response(400)
self.end_headers()
self.server.POSTbusy=False
return
print('**************** Reading message')
messageBytes=self.rfile.read(length)
messageJson = json.loads(messageBytes)
# check the necessary properties are available
print('**************** Check message has params')
if not inboxMessageHasParams(messageJson):
self.send_response(403)
self.end_headers()
self.server.POSTbusy=False
return
if not inboxPermittedMessage(self.server.domain,messageJson,self.server.federationList):
print('**************** Ah Ah Ah')
self.send_response(403)
self.end_headers()
self.server.POSTbusy=False
return
pprint(messageJson)
print('**************** POST create session')
currSessionTime=int(time.time())
if currSessionTime-self.server.sessionLastUpdate>1200:
self.server.sessionLastUpdate=currSessionTime
self.server.session = \
createSession(self.server.domain,self.server.port, \
self.server.useTor)
print('**************** POST started new session')
print('**************** POST get actor url from '+self.server.baseDir)
personUrl=messageJson['actor']
print('**************** POST get public key of '+personUrl+' from '+self.server.baseDir)
pubKey=getPersonPubKey(self.server.session,personUrl, \
self.server.personCache)
if not pubKey:
print('**************** POST no sender public key')
self.send_response(401)
self.end_headers()
self.server.POSTbusy=False
return
print('**************** POST check signature')
if not verifyPostHeaders(self.server.https, pubKey, self.headers, \
'/inbox' ,False, json.dumps(messageJson)):
print('**************** POST signature verification failed')
self.send_response(401)
self.end_headers()
self.server.POSTbusy=False
return
print('**************** POST valid')
if receiveFollowRequest(self.server.baseDir,messageJson, \
self.server.federationList):
self.send_response(200)
self.end_headers()
self.server.POSTbusy=False
return
pprint(messageJson)
# add a property to the object, just to mess with data
#message['received'] = 'ok'
# send the message back
#self._set_headers('application/json')
#self.wfile.write(json.dumps(message).encode('utf-8'))
self.server.receivedMessage=True
self.send_response(200)
self.end_headers()
self.server.POSTbusy=False
def runDaemon(domain: str,port=80,https=True,fedList=[],useTor=False) -> None:
if len(domain)==0:
domain='localhost'
if '.' not in domain:
print('Invalid domain: ' + domain)
return
serverAddress = ('', port)
httpd = ThreadingHTTPServer(serverAddress, PubServer)
httpd.domain=domain
httpd.port=port
httpd.https=https
httpd.federationList=fedList.copy()
httpd.baseDir=os.getcwd()
httpd.personCache={}
httpd.cachedWebfingers={}
httpd.useTor=useTor
httpd.session = None
httpd.sessionLastUpdate=0
httpd.lastGET=0
httpd.lastPOST=0
httpd.GETbusy=False
httpd.POSTbusy=False
httpd.receivedMessage=False
print('Running ActivityPub daemon on ' + domain + ' port ' + str(port))
httpd.serve_forever()