# This is a simple python flask implementation of 'How To Implement A Basic ActivityPub Server' # https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/ # © 2018 homebrewserver.club contributors # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . import flask, os from flask import request from flask import Response from time import strftime, gmtime import httpsig #Config DOMAIN = 'https://my-example.com' USERNAME = 'alice' def public_key(): """ Use commandline openssl to generate a public and private key """ if not os.path.exists('public.pem'): os.system('openssl genrsa -out private.pem 2048') os.system('openssl rsa -in private.pem -outform PEM -pubout -out public.pem') else: public_key = open('public.pem').read() public_key = public_key.replace('\n','\\n') #public key shouldn't contain verbatim linebreaks in json return public_key public_key() #generate public_key on first launch def sign_header(private_key, key_id, host): date= strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime()) keypair= open(private_key,'rb').read() hs = httpsig.HeaderSigner(key_id, secret, algorithm="rsa-sha256", headers=['(request-target): post /inbox', 'host', 'date']) auth = hs.sign({"Date": date, "Host": host}) # thanks to https://github.com/snarfed for the authorization -> signature headers hack # this is necessary because httpsig.HeaderSigner returns an Authorization header instead of Signature auth['Signature'] = auth.pop('authorization') assert auth['Signature'].startswith('Signature ') auth['Signature'] = auth['Signature'][len('Signature '):] #Flask app = flask.Flask(__name__) @app.route('/') def index(): return 'It works! Now try to look for a user@my-example.com via the mastodon interface' @app.route('/.well-known/webfinger') def finger(): """ Respond to webfinger queries (GET /.well-known/webfinger?resource=acct:alice@my-example.com) with a json object pointing to the actor see templates/webfinger.json """ if request.args.get('resource'): query = request.args.get('resource') actor = query.split(':')[1].split('@')[0] # from 'acct:alice@my-example.com' to 'alice' json = flask.render_template('webfinger.json', query=query, actor=actor, domain=DOMAIN) # render our ActivityPub answer return Response(response=json, status=200, mimetype="application/json") # return that answer as a json object @app.route('/users/') def profile(actor): """ Return an Actor object see templates/actor.json """ json = flask.render_template('actor.json', preferred_username=USERNAME, actor=actor, domain=DOMAIN, public_key=public_key()) # render our ActivityPub answer return Response(response=json, status=200, mimetype="application/json") # return that answer as a json object if __name__ == '__main__': app.debug =True app.run()