epicyon/daemon.py

231 lines
7.2 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, HTTPServer
#import socketserver
import json
import cgi
from pprint import pprint
from session import createSession
from httpsig import testHttpsig
2019-06-30 16:29:53 +00:00
from cache import testCache
2019-06-28 18:55:29 +00:00
from webfinger import webfingerMeta
from webfinger import webfingerLookup
from person import personLookup
from person import personKeyLookup
2019-06-29 14:35:26 +00:00
from person import personOutboxJson
2019-06-28 21:59:54 +00:00
from inbox import inboxPermittedMessage
2019-06-29 20:21:37 +00:00
from follow import getFollowingFeed
2019-06-30 16:50:43 +00:00
from threads import testThreads
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-28 20:22:36 +00:00
# domain name of this server
2019-06-28 18:55:29 +00:00
thisDomain=''
2019-06-28 20:22:36 +00:00
# List of domains to federate with
federationList=[]
2019-06-28 21:06:05 +00:00
# Avoid giant messages
maxMessageLength=5000
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-29 14:35:26 +00:00
# Whether to use https
useHttps=True
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:
username,domain = parseHandle(u)
if username:
followlist.append(username+'@'+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):
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:
if not self.path.startswith('/.well-known'):
return False
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
wfResult=webfingerLookup(self.path)
if wfResult:
self._set_headers('application/json')
self.wfile.write(json.dumps(wfResult).encode('utf-8'))
else:
self._404()
return True
2019-06-28 21:59:54 +00:00
def _permittedDir(self,path):
2019-06-28 18:55:29 +00:00
if path.startswith('/wfendpoints') or \
path.startswith('/keys') or \
path.startswith('/accounts'):
return False
return True
def do_GET(self):
2019-06-29 17:25:09 +00:00
try:
if self.GETbusy:
2019-06-29 17:28:43 +00:00
self.send_response(429)
2019-06-29 17:25:09 +00:00
self.end_headers()
return
except:
pass
self.GETbusy=True
2019-06-28 21:59:54 +00:00
if not self._permittedDir(self.path):
2019-06-28 18:55:29 +00:00
self._404()
2019-06-29 17:25:09 +00:00
self.GETbusy=False
2019-06-28 18:55:29 +00:00
return
# get webfinger endpoint for a person
if self._webfinger():
2019-06-29 17:25:09 +00:00
self.GETbusy=False
2019-06-28 18:55:29 +00:00
return
2019-06-29 14:35:26 +00:00
# get outbox feed for a person
2019-06-29 14:41:23 +00:00
outboxFeed=personOutboxJson(thisDomain,self.path,useHttps,maxPostsInFeed)
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-06-29 17:25:09 +00:00
self.GETbusy=False
2019-06-29 20:21:37 +00:00
return
following=getFollowingFeed(thisDomain,self.path,useHttps,followsPerPage)
if following:
self._set_headers('application/json')
self.wfile.write(json.dumps(following).encode('utf-8'))
self.GETbusy=False
return
followers=getFollowingFeed(thisDomain,self.path,useHttps,followsPerPage,'followers')
if followers:
self._set_headers('application/json')
self.wfile.write(json.dumps(followers).encode('utf-8'))
self.GETbusy=False
2019-06-29 14:35:26 +00:00
return
2019-06-28 18:55:29 +00:00
# look up a person
getPerson = personLookup(thisDomain,self.path)
if getPerson:
self._set_headers('application/json')
self.wfile.write(json.dumps(getPerson).encode('utf-8'))
2019-06-29 17:25:09 +00:00
self.GETbusy=False
2019-06-28 18:55:29 +00:00
return
2019-06-30 15:03:26 +00:00
personKey = personKeyLookup(thisDomain,self.path)
if personKey:
2019-06-28 18:55:29 +00:00
self._set_headers('text/html; charset=utf-8')
2019-06-30 15:03:26 +00:00
self.wfile.write(personKey.encode('utf-8'))
2019-06-29 17:25:09 +00:00
self.GETbusy=False
2019-06-28 18:55:29 +00:00
return
# check that a json file was requested
if not self.path.endswith('.json'):
self._404()
2019-06-29 17:25:09 +00:00
self.GETbusy=False
2019-06-28 18:55:29 +00:00
return
# check that the file exists
2019-06-29 20:21:37 +00:00
baseDir=os.getcwd()
2019-06-28 18:55:29 +00:00
filename=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:
self._404()
2019-06-29 17:25:09 +00:00
self.GETbusy=False
2019-06-28 18:55:29 +00:00
def do_HEAD(self):
self._set_headers('application/json')
def do_POST(self):
2019-06-29 17:27:32 +00:00
try:
if self.POSTbusy:
2019-06-29 17:28:43 +00:00
self.send_response(429)
2019-06-29 17:27:32 +00:00
self.end_headers()
return
except:
pass
self.POSTbusy=True
2019-06-28 18:55:29 +00:00
ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
# refuse to receive non-json content
if ctype != 'application/json':
self.send_response(400)
self.end_headers()
2019-06-29 17:27:32 +00:00
self.POSTbusy=False
2019-06-28 18:55:29 +00:00
return
# read the message and convert it into a python dictionary
length = int(self.headers.getheader('content-length'))
2019-06-28 21:06:05 +00:00
if length>maxMessageLength:
self.send_response(400)
self.end_headers()
2019-06-29 17:27:32 +00:00
self.POSTbusy=False
2019-06-28 21:06:05 +00:00
return
message = json.loads(self.rfile.read(length))
2019-06-28 20:35:34 +00:00
2019-06-28 21:59:54 +00:00
if not inboxPermittedMessage(message,federationList):
self.send_response(403)
self.end_headers()
2019-06-28 20:35:34 +00:00
else:
# add a property to the object, just to mess with data
message['received'] = 'ok'
2019-06-28 18:55:29 +00:00
2019-06-28 20:35:34 +00:00
# send the message back
self._set_headers('application/json')
self.wfile.write(json.dumps(message).encode('utf-8'))
2019-06-29 17:27:32 +00:00
self.POSTbusy=False
2019-06-28 18:55:29 +00:00
2019-06-28 21:07:21 +00:00
def runDaemon(domain: str,port=80,fedList=[],useTor=False) -> None:
2019-06-28 18:55:29 +00:00
global thisDomain
2019-06-28 20:22:36 +00:00
global federationList
2019-06-28 18:55:29 +00:00
thisDomain=domain
2019-06-28 20:43:37 +00:00
federationList=fedList.copy()
2019-06-28 18:55:29 +00:00
if len(domain)==0:
domain='127.0.0.1'
if '.' not in domain:
print('Invalid domain: ' + domain)
return
session = createSession(useTor)
print('Running tests...')
testHttpsig()
2019-06-30 16:29:53 +00:00
testCache()
2019-06-30 16:50:43 +00:00
testThreads()
2019-06-28 18:55:29 +00:00
print('Tests succeeded\n')
serverAddress = ('', port)
httpd = HTTPServer(serverAddress, PubServer)
print('Running ActivityPub daemon on ' + thisDomain + ' port ' + str(port))
httpd.serve_forever()