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
|
|
|
|
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-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
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
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-28 21:59:54 +00:00
|
|
|
if not self._permittedDir(self.path):
|
2019-06-28 18:55:29 +00:00
|
|
|
self._404()
|
|
|
|
return
|
|
|
|
# get webfinger endpoint for a person
|
|
|
|
if self._webfinger():
|
|
|
|
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 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'))
|
|
|
|
return
|
|
|
|
getPersonKey = personKeyLookup(thisDomain,self.path)
|
|
|
|
if getPersonKey:
|
|
|
|
self._set_headers('text/html; charset=utf-8')
|
|
|
|
self.wfile.write(getPersonKey.encode('utf-8'))
|
|
|
|
return
|
|
|
|
# check that a json file was requested
|
|
|
|
baseDir=os.getcwd()
|
|
|
|
if not self.path.endswith('.json'):
|
|
|
|
self._404()
|
|
|
|
return
|
|
|
|
# check that the file exists
|
|
|
|
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()
|
|
|
|
|
|
|
|
def do_HEAD(self):
|
|
|
|
self._set_headers('application/json')
|
|
|
|
|
|
|
|
def do_POST(self):
|
|
|
|
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()
|
|
|
|
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()
|
|
|
|
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-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()
|
|
|
|
print('Tests succeeded\n')
|
|
|
|
|
|
|
|
serverAddress = ('', port)
|
|
|
|
httpd = HTTPServer(serverAddress, PubServer)
|
|
|
|
print('Running ActivityPub daemon on ' + thisDomain + ' port ' + str(port))
|
|
|
|
httpd.serve_forever()
|