From 54ee1b35946b587e85c8995ed10b9d5eecab802d Mon Sep 17 00:00:00 2001 From: jesopo Date: Sun, 15 Sep 2019 10:43:46 +0100 Subject: re-merge fediverse an fediverse_server, so they can share utils --- modules/fediverse.py | 140 -------------------- modules/fediverse/__init__.py | 87 +++++++++++++ modules/fediverse/activities.py | 26 ++++ modules/fediverse/ap_actor.py | 55 ++++++++ modules/fediverse/ap_utils.py | 56 ++++++++ modules/fediverse/security.py | 36 +++++ modules/fediverse/server.py | 246 +++++++++++++++++++++++++++++++++++ modules/fediverse_server/__init__.py | 226 -------------------------------- modules/fediverse_server/security.py | 32 ----- 9 files changed, 506 insertions(+), 398 deletions(-) delete mode 100644 modules/fediverse.py create mode 100644 modules/fediverse/__init__.py create mode 100644 modules/fediverse/activities.py create mode 100644 modules/fediverse/ap_actor.py create mode 100644 modules/fediverse/ap_utils.py create mode 100644 modules/fediverse/security.py create mode 100644 modules/fediverse/server.py delete mode 100644 modules/fediverse_server/__init__.py delete mode 100644 modules/fediverse_server/security.py (limited to 'modules') diff --git a/modules/fediverse.py b/modules/fediverse.py deleted file mode 100644 index bb464de0..00000000 --- a/modules/fediverse.py +++ /dev/null @@ -1,140 +0,0 @@ -import urllib.parse -from src import IRCBot, ModuleManager, utils - -HOSTMETA = "https://%s/.well-known/host-meta" -WEBFINGER_DEFAULT = "https://%s/.well-known/webfinger?resource={uri}" -WEBFINGER_HEADERS = {"Accept": "application/jrd+json"} - -ACTIVITY_TYPE = "application/activity+json" -ACTIVITY_HEADERS = {"Accept": ("application/ld+json; " - 'profile="https://www.w3.org/ns/activitystreams"')} - -USERAGENT = "BitBot (%s) Fediverse" % IRCBot.VERSION - -def _parse_username(s): - username, _, instance = s.rpartition("@") - if username.startswith("@"): - username = username[1:] - if username and instance: - return username, instance - return None, None -def _format_username(username, instance): - return "@%s@%s" % (username, instance) -def _setting_parse(s): - username, instance = _parse_username(s) - if username and instance: - return _format_username(username, instance) - return None - -@utils.export("set", utils.FunctionSetting(_setting_parse, "fediverse", - help="Set your fediverse account", example="@gargron@mastodon.social")) -class Module(ModuleManager.BaseModule): - _name = "Fedi" - - @utils.hook("received.command.fediverse") - @utils.hook("received.command.fedi", alias_of="fediverse") - @utils.kwarg("help", "Get someone's latest toot") - @utils.kwarg("usage", "@@") - def fedi(self, event): - account = None - if not event["args"]: - account = event["user"].get_setting("fediverse", None) - elif not "@" in event["args"]: - target = event["args_split"][0] - if event["server"].has_user_id(target): - target_user = event["server"].get_user(target) - account = target_user.get_setting("fediverse", None) - else: - account = event["args_split"][0] - - username = None - instance = None - if account: - username, instance = _parse_username(account) - - if not username or not instance: - raise utils.EventError("Please provide @@") - - hostmeta = utils.http.request(HOSTMETA % instance, - parse=True, check_content_type=False, useragent=USERAGENT) - webfinger_url = None - for item in hostmeta.data.find_all("link"): - if item["rel"] and item["rel"][0] == "lrdd": - webfinger_url = item["template"] - break - - if webfinger_url == None: - self.log.debug("host-meta lookup failed for %s" % instance) - webfinger_url = WEBFINGER_DEFAULT % instance - webfinger_url = webfinger_url.replace("{uri}", - "acct:%s@%s" % (username, instance)) - - webfinger = utils.http.request(webfinger_url, - headers=WEBFINGER_HEADERS, json=True, useragent=USERAGENT) - - activity_url = None - for link in webfinger.data["links"]: - if link["type"] == ACTIVITY_TYPE: - activity_url = link["href"] - break - - if not activity_url: - raise utils.EventError("Failed to find user activity feed") - - activity = utils.http.request(activity_url, - headers=ACTIVITY_HEADERS, json=True, useragent=USERAGENT) - preferred_username = activity.data["preferredUsername"] - outbox_url = activity.data["outbox"] - - outbox = utils.http.request(outbox_url, headers=ACTIVITY_HEADERS, - json=True, useragent=USERAGENT) - items = None - - if "first" in outbox.data: - if type(outbox.data["first"]) == dict: - # pleroma - items = outbox.data["first"]["orderedItems"] - else: - # mastodon - first = utils.http.request(outbox.data["first"], - headers=ACTIVITY_HEADERS, json=True, useragent=USERAGENT) - items = first.data["orderedItems"] - else: - items = outbox.data["orderedItems"] - - if not items: - raise utils.EventError("No toots found") - - first_item = items[0] - if first_item["type"] == "Announce": - retoot_url = first_item["object"] - retoot_instance = urllib.parse.urlparse(retoot_url).hostname - retoot = utils.http.request(retoot_url, - headers=ACTIVITY_HEADERS, json=True, useragent=USERAGENT) - - original_tooter_url = retoot.data["attributedTo"] - original_tooter = utils.http.request(original_tooter_url, - headers=ACTIVITY_HEADERS, json=True, useragent=USERAGENT) - - retooted_user = "@%s@%s" % ( - original_tooter.data["preferredUsername"], - retoot_instance) - - shorturl = self.exports.get_one("shorturl")( - event["server"], retoot_url) - retoot_content = utils.http.strip_html( - retoot.data["content"]) - - event["stdout"].write("%s (boost %s): %s - %s" % ( - preferred_username, retooted_user, retoot_content, - shorturl)) - - elif first_item["type"] == "Create": - content = utils.http.strip_html( - first_item["object"]["content"]) - url = first_item["object"]["id"] - shorturl = self.exports.get_one("shorturl")( - event["server"], url) - - event["stdout"].write("%s: %s - %s" % (preferred_username, - content, shorturl)) diff --git a/modules/fediverse/__init__.py b/modules/fediverse/__init__.py new file mode 100644 index 00000000..41e0bfe6 --- /dev/null +++ b/modules/fediverse/__init__.py @@ -0,0 +1,87 @@ +import urllib.parse +from src import IRCBot, ModuleManager, utils +from . import ap_actor, ap_utils + +def _format_username(username, instance): + return "@%s@%s" % (username, instance) +def _setting_parse(s): + username, instance = ap_utils.split_username(s) + if username and instance: + return _format_username(username, instance) + return None + +@utils.export("set", utils.FunctionSetting(_setting_parse, "fediverse", + help="Set your fediverse account", example="@gargron@mastodon.social")) +class Module(ModuleManager.BaseModule): + _name = "Fedi" + + @utils.hook("received.command.fediverse") + @utils.hook("received.command.fedi", alias_of="fediverse") + @utils.kwarg("help", "Get someone's latest toot") + @utils.kwarg("usage", "@@") + def fedi(self, event): + account = None + if not event["args"]: + account = event["user"].get_setting("fediverse", None) + elif not "@" in event["args"]: + target = event["args_split"][0] + if event["server"].has_user_id(target): + target_user = event["server"].get_user(target) + account = target_user.get_setting("fediverse", None) + else: + account = event["args_split"][0] + + username = None + instance = None + if account: + username, instance = ap_utils.split_username(account) + + if not username or not instance: + raise utils.EventError("Please provide @@") + + actor_url = ap_utils.find_actor(username, instance) + + if not actor_url: + raise utils.EventError("Failed to find actor") + + actor = ap_actor.Actor(actor_url) + actor.load() + items = actor.outbox.load() + + if not items: + raise utils.EventError("No toots found") + + first_item = items[0] + if first_item["type"] == "Announce": + retoot_url = first_item["object"] + retoot_instance = urllib.parse.urlparse(retoot_url).hostname + retoot = utils.http.request(retoot_url, + headers=ACTIVITY_HEADERS, json=True, useragent=USERAGENT) + + original_tooter = ap_actor.Actor(retoot.data["attributedTo"]) + original_tooter.load() + + original_tooter = utils.http.request(original_tooter_url, + headers=ACTIVITY_HEADERS, json=True, useragent=USERAGENT) + + retooted_user = "@%s@%s" % (original_tooter.username, + retoot_instance) + + shorturl = self.exports.get_one("shorturl")( + event["server"], retoot_url) + retoot_content = utils.http.strip_html( + retoot.data["content"]) + + event["stdout"].write("%s (boost %s): %s - %s" % ( + actor.username, retooted_user, retoot_content, + shorturl)) + + elif first_item["type"] == "Create": + content = utils.http.strip_html( + first_item["object"]["content"]) + url = first_item["object"]["id"] + shorturl = self.exports.get_one("shorturl")( + event["server"], url) + + event["stdout"].write("%s: %s - %s" % (actor.username, + content, shorturl)) diff --git a/modules/fediverse/activities.py b/modules/fediverse/activities.py new file mode 100644 index 00000000..8a7371c1 --- /dev/null +++ b/modules/fediverse/activities.py @@ -0,0 +1,26 @@ +from . import utils as ap_utils + +class Activity(object): + _type = "" + def __init__(self, id, object): + self._id = id + self._object = object + def format(self, actor): + return { + "@context": "https://www.w3.org/ns/activitystreams", + "actor": actor.url, + "id": self._id, + "object": self._object, + "type": self._type + } + +class Follow(Activity): + _type = "Follow" +class Accept(Activity): + _type = "Accept" + +class Create(Activity): + _type = "Create" + +class Announce(Activity): + _type = "Announce" diff --git a/modules/fediverse/ap_actor.py b/modules/fediverse/ap_actor.py new file mode 100644 index 00000000..3f12de0d --- /dev/null +++ b/modules/fediverse/ap_actor.py @@ -0,0 +1,55 @@ +import email.utils +from src import utils +from . import ap_utils + +class Actor(object): + def __init__(self, url): + self.url = url + + self.username = None + self.inbox = None + self.outbox = None + + def load(self): + data = ap_utils.activity_request(self.url) + self.username = data["preferredUsername"] + self.inbox = Inbox(data["inbox"]) + self.outbox = Outbox(data["outbox"]) + +class Outbox(object): + def __init__(self, url): + self._url = url + + def load(self): + outbox = ap_utils.activity_request(self._url) + + items = None + if "first" in outbox: + if type(outbox["first"]) == dict: + # pleroma + items = outbox["first"]["orderedItems"] + else: + # mastodon + first = ap_utils.activity_request(outbox["first"]) + items = first["orderedItems"] + else: + items = outbox["orderedItems"] + return items + +class Inbox(object): + def __init__(self, url): + self._url = url + def send(activity, private_key): + now = email.utils.formatdate(timeval=None, localtime=False, usegmt=True) + parts = urllib.parse.urlparse(self._url) + headers = [ + ["host", parts.netloc], + ["date", now] + ] + sign_headers = headers[:] + sign_headers.insert(0, ["(request-target)", "post %s" % parts.path]) + signature = security.signature(private_key.key, sign_headers) + + return ap_utils.request(self._url, activity.format(self), + method="POST", private_key=private_key) + diff --git a/modules/fediverse/ap_utils.py b/modules/fediverse/ap_utils.py new file mode 100644 index 00000000..3e72d7ea --- /dev/null +++ b/modules/fediverse/ap_utils.py @@ -0,0 +1,56 @@ +from src import IRCBot, utils + +LD_TYPE = ("application/ld+json; " + "profile=\"https://www.w3.org/ns/activitystreams\"") +JRD_TYPE = "application/jrd+json" +ACTIVITY_TYPE = "application/activity+json" +USERAGENT = "BitBot (%s) Fediverse" % IRCBot.VERSION + +def split_username(s): + if s[0] == "@": + s = s[1:] + username, _, instance = s.partition("@") + if username and instance: + return username, instance + return None, None + +def activity_request(url, data=None, method="GET", type=ACTIVITY_TYPE): + content_type = None + headers = {} + + if method == "POST": + content_type = type + else: + headers = {"Accept": type} + + request = utils.http.Request(url, headers=headers, useragent=USERAGENT, + content_type=content_type, data=data, json=True) + return utils.http.request(request).data + +HOSTMETA_TEMPLATE = "https://%s/.well-known/host-meta" +WEBFINGER_TEMPLATE = "https://%s/.well-known/webfinger?resource={uri}" + +def find_actor(username, instance): + hostmeta = HOSTMETA_TEMPLATE % instance + hostmeta_request = utils.http.Request(HOSTMETA_TEMPLATE % instance, + useragent=USERAGENT, parse=True, check_content_type=False) + hostmeta = utils.http.request(hostmeta_request) + + webfinger_url = None + for item in hostmeta.data.find_all("link"): + if item["rel"] and item["rel"][0] == "lrdd": + webfinger_url = item["template"] + break + + if not webfinger_url: + webfinger_url = WEBFINGER_TEMPLATE % instance + webfinger_url = webfinger_url.replace("{uri}", + "acct:%s@%s" % (username, instance), 1) + + webfinger = activity_request(webfinger_url, type=JRD_TYPE) + + actor_url = None + for link in webfinger["links"]: + if link["type"] == ACTIVITY_TYPE: + return link["href"] + diff --git a/modules/fediverse/security.py b/modules/fediverse/security.py new file mode 100644 index 00000000..00fe8264 --- /dev/null +++ b/modules/fediverse/security.py @@ -0,0 +1,36 @@ +import base64, typing +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding, rsa +from cryptography.hazmat.backends import default_backend + +SIGNATURE_FORMAT = ( + "keyId=\"%s\",headers=\"%s\",signature=\"%s\",algorithm=\"rsa-sha256\"") + + +def _private_key(key_filename: str) -> rsa.RSAPrivateKey: + with open(key_filename) as key_file: + return serialization.load_pem_private_key( + key_file.read(), password=None, backend=default_backend()) + +class PrivateKey(object): + def __init__(self, filename, id): + self.key = _private_key(filename) + self.id = id + +def signature(key: PrivateKey, headers: typing.List[typing.Tuple[str, str]] + ) -> str: + sign_header_keys = " ".join(h[0] for h in headers) + + sign_string_parts = ["%s: %s" % (k, v) for k, v in headers] + sign_string = "\n".join(sign_string_parts) + + signature = key.key.sign( + sign_string.encode("utf8"), + padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), + salt_length=padding.PSS.MAX_LENGTH), + hashes.SHA256() + ) + + signature = base64.b64encode(signature).decode("ascii") + return SIGNATURE_FORMAT % (key_id, sign_header_keys, signature) diff --git a/modules/fediverse/server.py b/modules/fediverse/server.py new file mode 100644 index 00000000..80e1654a --- /dev/null +++ b/modules/fediverse/server.py @@ -0,0 +1,246 @@ +#--require-config tls-certificate + +import base64, binascii, os, urllib.parse +from src import ModuleManager, utils + +from . import actor as ap_actor +from . import activities as ap_activities +from . import security as ap_security + +ACTIVITY_SETTING_PREFIX = "ap-activity-" + +def _parse_username(s): + username, _, instance = s.rpartition("@") + if username.startswith("@"): + username = username[1:] + if username and instance: + return username, instance + return None, None +def _format_username(username, instance): + return "@%s@%s" % (username, instance) +def _setting_parse(s): + username, instance = _parse_username(s) + if username and instance: + return _format_username(username, instance) + return None + +@utils.export("botset", utils.FunctionSetting(_setting_parse, "fediverse", + help="Set the bot's fediverse server account", + example="@gargron@mastodon.social")) +class Module(ModuleManager.BaseModule): + _name = "Fedi" + + def _random_id(self): + return binascii.hexlify(os.urandom(3)).decode("ascii") + + def _get_activities(self): + activities = [] + for setting, (content, timestamp) in self.bot.find_settings_prefix( + ACTIVITY_SETTING_PREFIX): + activity_id = setting.replace(ACTIVITY_SETTING_PREFIX, "", 1) + activities.append([activity_id, content, timestamp]) + return activities + def _make_activity(self, content): + timestamp = utils.iso8601_format_now() + activity_id = self._random_id() + self.bot.set_setting("ap-activity-%s" % activity_id, + [content, timestamp]) + return activity_id + + @utils.hook("received.command.toot") + @utils.kwarg("min_args", 1) + @utils.kwarg("permission", "fediverse") + def toot(self, event): + activity_id = self._make_activity(event["args"]) + event["stdout"].write("Sent toot %s" % activity_id) + + @utils.hook("received.command.fedifollow") + @utils.kwarg("min_args", 1) + @utils.kwarg("permission", "fediverse") + def fedi_follow(self, event): + pass + + def _toot(self, activity_id): + our_username, our_instance = self._ap_self() + content, timestamp = self.bot.get_setting( + "ap-activity-%s" % activity_id) + url_for = self.exports.get_one("url-for") + self_id = self._ap_self_url(url_for, our_username) + activity_url = self._ap_activity_url(url_for, activity_id) + + object = { + "id": activity_url, + "type": "Note", + "published": timestamp, + "attributedTo": self_id, + "content": content, + "to": "https://www.w3.org/ns/activitystreams#Public" + } + activity = ap_activities.Create(activity_url, object) + + private_key = self._private_key() + + for actor_url in self._get_actors(): + actor = ap_actor.Actor(actor_url) + actor.load() + actor.inbox.send(activity, private_key) + + def _ap_self(self): + our_username = self.bot.get_setting("fediverse", None) + return _parse_username(our_username) + + def _ap_url(self, url_for, fragment, kwargs): + return "https://%s" % url_for("api", fragment, kwargs) + def _ap_self_url(self, url_for, our_username): + return self._ap_url(url_for, "ap-user", {"u": our_username}) + def _ap_inbox_url(self, url_for, our_username): + return self._ap_url(url_for, "ap-inbox", {"u": our_username}) + def _ap_outbox_url(self, url_for, our_username): + return self._ap_url(url_for, "ap-outbox", {"u": our_username}) + def _ap_activity_url(self, url_for, activity_id): + return self._ap_url(url_for, "ap-activity", {"a": activity_id}) + def _ap_keyid_url(self, url_for, our_username): + return "%s#key" % self._ap_self_url(url_for, our_username) + + @utils.hook("api.get.ap-webfinger") + @utils.kwarg("authenticated", False) + def ap_webfinger(self, event): + our_username, our_instance = self._ap_self() + + resource = event["params"].get("resource", None) + if resource.startswith("acct:"): + resource = resource.split(":", 1)[1] + + if resource: + requested_username, requested_instance = _parse_username(resource) + + if (requested_username == our_username and + requested_instance == our_instance): + + self_id = self._ap_self_url(event["url_for"], our_username) + + event["response"].content_type = consts.JRD_TYPE + event["response"].write_json({ + "aliases": [self_id], + "links": [{ + "href": self_id, + "rel": "self", + "type": consts.ACTIVITY_TYPE + }], + "subject": "acct:%s" % resource + }) + else: + event["response"].code = 404 + else: + event["response"].code = 400 + + @utils.hook("api.get.ap-user") + @utils.kwarg("authenticated", False) + def ap_user(self, event): + our_username, our_instance = self._ap_self() + username = event["params"].get("u", None) + + if username and username == our_username: + self_id = self._ap_self_url(event["url_for"], our_username) + inbox = self._ap_inbox_url(event["url_for"], our_username) + outbox = self._ap_outbox_url(event["url_for"], our_username) + + cert_filename = self.bot.config["tls-certificate"] + with open(cert_filename) as cert_file: + cert = cert_file.read().strip() + + event["response"].content_type = consts.LD_TYPE + event["response"].write_json({ + "@context": "https://www.w3.org/ns/activitystreams", + "id": self_id, "url": self_id, + "type": "Person", + "summary": "beep boop", + "preferredUsername": our_username, "name": our_username, + "inbox": inbox, + "outbox": outbox, + "publicKey": { + "id": "%s#key" % self_id, + "owner": self_id, + "publicKeyPem": cert + } + }) + else: + event["response"].code = 404 + + def _prepare_activity(self, url_for, self_id, activity_id, content, + timestamp): + activity_url = self._ap_activity_url(url_for, activity_id) + context = "data:%s" % activity_id + return activity_url, { + "attributedTo": self_id, + "content": content, + "conversation": context, "context": context, + "id": activity_url, "url": activity_url, + "published": timestamp, + "summary": "", # content warning here + "to": "https://www.w3.org/ns/activitystreams#Public", + "type": "Note", + } + + @utils.hook("api.get.ap-outbox") + @utils.kwarg("authenticated", False) + def ap_outbox(self, event): + our_username, our_instance = self._ap_self() + username = event["params"].get("u", None) + if username and username == our_username: + self_id = self._ap_self_url(event["url_for"], our_username) + outbox = self._ap_outbox_url(event["url_for"], our_username) + + activities = [] + for activity_id, content, timestamp in self._get_activities(): + activity_url, activity_object = self._prepare_activity( + event["url_for"], self_id, activity_id, content, timestamp) + activities.append({ + "actor": self_id, + "id": activity_url, + "object": activity_object, + "published": timestamp, + "to": "https://www.w3.org/ns/activitystreams#Public", + "type": "Create" + }) + + event["response"].content_type = consts.LD_TYPE + event["response"].write_json({ + "@context": "https://www.w3.org/ns/activitystreams", + "id": outbox, + "orderedItems": activities, + "totalItems": len(activities), + "type": "OrderedCollection" + }) + + else: + event["response"].code = 404 + + def _private_key(self): + id = self._ap_keyid_url(url_for, our_username) + filename = security.private_key(self.bot.config["tls-certificate"]) + return ap_security.PrivateKey(filename, id) + + @utils.hook("api.post.ap-inbox") + @utils.kwarg("authenticated", False) + def ap_inbox(self, event): + data = json.loads(event["data"]) + self_id = self._ap_self_url(event["url_for"], our_username) + + if data["type"] == "Follow": + if data["object"] == self_id: + new_follower = data["actor"] + followers = set(self.bot.get_setting("fediverse-followers", [])) + if not new_follower in followers: + followers.add(new_follower) + + private_key = self._private_key() + actor = ap_actor.Actor(new_follower) + accept = ap_activities.Accept(data["id"], data) + actor.inbox.send(accept, private_key) + + follow_id = "data:%s" % str(uuid.uuid4()) + follow = ap_activities.Follow(follow_id, self_id) + actor.inbox.send(follow, private_key) + else: + event["response"].code = 404 diff --git a/modules/fediverse_server/__init__.py b/modules/fediverse_server/__init__.py deleted file mode 100644 index 82f03fc8..00000000 --- a/modules/fediverse_server/__init__.py +++ /dev/null @@ -1,226 +0,0 @@ -#--require-config tls-certificate - -import base64, binascii, os, urllib.parse -from src import ModuleManager, utils - -from cryptography.hazmat.primitives import serialization, hashes -from cryptography.hazmat.primitives.asymmetric import padding -from cryptography.hazmat.backends import default_backend - -LD_TYPE = ("application/ld+json; " - "profile=\"https://www.w3.org/ns/activitystreams\"") -JRD_TYPE = "application/jrd+json" -ACTIVITY_TYPE = "application/activity+json" - -ACTIVITY_SETTING_PREFIX = "ap-activity-" - -def _parse_username(s): - username, _, instance = s.rpartition("@") - if username.startswith("@"): - username = username[1:] - if username and instance: - return username, instance - return None, None -def _format_username(username, instance): - return "@%s@%s" % (username, instance) -def _setting_parse(s): - username, instance = _parse_username(s) - if username and instance: - return _format_username(username, instance) - return None - -@utils.export("botset", utils.FunctionSetting(_setting_parse, "fediverse", - help="Set the bot's fediverse server account", - example="@gargron@mastodon.social")) -class Module(ModuleManager.BaseModule): - _name = "Fedi" - - def _random_id(self): - return binascii.hexlify(os.urandom(3)).decode("ascii") - - def _get_activities(self): - activities = [] - for setting, (content, timestamp) in self.bot.find_settings_prefix( - ACTIVITY_SETTING_PREFIX): - activity_id = setting.replace(ACTIVITY_SETTING_PREFIX, "", 1) - activities.append([activity_id, content, timestamp]) - return activities - def _make_activity(self, content): - timestamp = utils.iso8601_format_now() - activity_id = self._random_id() - self.bot.set_setting("ap-activity-%s" % activity_id, - [content, timestamp]) - return activity_id - - @utils.hook("received.command.toot") - @utils.kwarg("min_args", 1) - @utils.kwarg("permission", "toot") - def toot(self, event): - activity_id = self._make_activity(event["args"]) - event["stdout"].write("Sent toot %s" % activity_id) - - def _federate_activity(self, activity_id, content, timestamp): - - message = { - "@context": "https://www.w3.org/ns/activitystreams", - "type": "Announce", - "to": [], - "actor": "", - "object": "" - } - - - def _federate(self, data): - our_username, our_instance = self._ap_self() - key_id = self._ap_keyid_url(url_for, our_username) - now = email.utils.formatdate(timeval=None, localtime=False, usegmt=True) - url_for = self.exports.get_one("url-for") - - key = security.private_key(self.bot.config["tls-certificate"]) - - for inbox in self._get_inboxes(): - parts = urllib.parse.urlparse(inbox) - headers = [ - ["host", parts.netloc], - ["date", now] - ] - sign_headers = headers[:] - sign_headers.insert(0, ["(request-target)", "post %s" % parts.path]) - - signature = security.signature(key, key_id, sign_headers) - data = "" - request = utils.http.Request(inbox, data=data, headers=headers, - content_type=ACTIVITY_TYPE, useragent="BitBot Fediverse") - utils.http.request() - - def _ap_self(self): - our_username = self.bot.get_setting("fediverse", None) - return _parse_username(our_username) - - def _ap_url(self, url_for, fragment, kwargs): - return "https://%s" % url_for("api", fragment, kwargs) - def _ap_self_url(self, url_for, our_username): - return self._ap_url(url_for, "ap-user", {"u": our_username}) - def _ap_inbox_url(self, url_for, our_username): - return self._ap_url(url_for, "ap-inbox", {"u": our_username}) - def _ap_outbox_url(self, url_for, our_username): - return self._ap_url(url_for, "ap-outbox", {"u": our_username}) - def _ap_activity_url(self, url_for, activity_id): - return self._ap_url(url_for, "ap-activity", {"a": activity_id}) - def _ap_keyid_url(self, url_for, our_username): - return "%s#key" % self._ap_self_url(url_for, our_username) - - @utils.hook("api.get.ap-webfinger") - @utils.kwarg("authenticated", False) - def ap_webfinger(self, event): - our_username, our_instance = self._ap_self() - - resource = event["params"].get("resource", None) - if resource.startswith("acct:"): - resource = resource.split(":", 1)[1] - - if resource: - requested_username, requested_instance = _parse_username(resource) - - if (requested_username == our_username and - requested_instance == our_instance): - - self_id = self._ap_self_url(event["url_for"], our_username) - - event["response"].content_type = JRD_TYPE - event["response"].write_json({ - "aliases": [self_id], - "links": [{ - "href": self_id, - "rel": "self", - "type": ACTIVITY_TYPE - }], - "subject": "acct:%s" % resource - }) - else: - event["response"].code = 404 - else: - event["response"].code = 400 - - @utils.hook("api.get.ap-user") - @utils.kwarg("authenticated", False) - def ap_user(self, event): - our_username, our_instance = self._ap_self() - username = event["params"].get("u", None) - - if username and username == our_username: - self_id = self._ap_self_url(event["url_for"], our_username) - inbox = self._ap_inbox_url(event["url_for"], our_username) - outbox = self._ap_outbox_url(event["url_for"], our_username) - - cert_filename = self.bot.config["tls-certificate"] - with open(cert_filename) as cert_file: - cert = cert_file.read().strip() - - event["response"].content_type = LD_TYPE - event["response"].write_json({ - "@context": "https://www.w3.org/ns/activitystreams", - "id": self_id, "url": self_id, - "type": "Person", - "summary": "beep boop", - "preferredUsername": our_username, "name": our_username, - "inbox": inbox, - "outbox": outbox, - "publicKey": { - "id": "%s#key" % self_id, - "owner": self_id, - "publicKeyPem": cert - } - }) - else: - event["response"].code = 404 - - def _prepare_activity(self, url_for, self_id, activity_id, content, - timestamp): - activity_url = self._ap_activity_url(url_for, activity_id) - context = "data:%s" % activity_id - return activity_url, { - "attributedTo": self_id, - "content": content, - "conversation": context, "context": context, - "id": activity_url, "url": activity_url, - "published": timestamp, - "summary": "", # content warning here - "to": "https://www.w3.org/ns/activitystreams#Public", - "type": "Note", - } - - @utils.hook("api.get.ap-outbox") - @utils.kwarg("authenticated", False) - def ap_outbox(self, event): - our_username, our_instance = self._ap_self() - username = event["params"].get("u", None) - if username and username == our_username: - self_id = self._ap_self_url(event["url_for"], our_username) - outbox = self._ap_outbox_url(event["url_for"], our_username) - - activities = [] - for activity_id, content, timestamp in self._get_activities(): - activity_url, activity_object = self._prepare_activity( - event["url_for"], self_id, activity_id, content, timestamp) - activities.append({ - "actor": self_id, - "id": activity_url, - "object": activity_object, - "published": timestamp, - "to": "https://www.w3.org/ns/activitystreams#Public", - "type": "Create" - }) - - event["response"].content_type = LD_TYPE - event["response"].write_json({ - "@context": "https://www.w3.org/ns/activitystreams", - "id": outbox, - "orderedItems": activities, - "totalItems": len(activities), - "type": "OrderedCollection" - }) - - else: - event["response"].code = 404 - diff --git a/modules/fediverse_server/security.py b/modules/fediverse_server/security.py deleted file mode 100644 index 6ae75cd3..00000000 --- a/modules/fediverse_server/security.py +++ /dev/null @@ -1,32 +0,0 @@ -import base64, typing -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import padding, rsa -from cryptography.hazmat.backends import default_backend - -SIGNATURE_FORMAT = ( - "keyId=\"%s\",headers=\"%s\",signature=\"%s\",algorithm=\"rsa-sha256\"") - - -def private_key(key_filename: str) -> rsa.RSAPrivateKey: - with open(key_filename) as key_file: - return serialization.load_pem_private_key( - key_file.read(), password=None, backend=default_backend()) - -def signature(key: rsa.RSAPrivateKey, key_id: str, - headers: typing.List[typing.Tuple[str, str]]) -> str: - private_key = _private_key(key_filename) - sign_header_keys = " ".join(h[0] for h in headers) - - sign_string_parts = ["%s: %s" % (k, v) for k, v in headers] - sign_string = "\n".join(sign_string_parts) - - signature = private_key.sign( - sign_string.encode("utf8"), - padding.PSS( - mgf=padding.MGF1(hashes.SHA256()), - salt_length=padding.PSS.MAX_LENGTH), - hashes.SHA256() - ) - - signature = base64.b64encode(signature).decode("ascii") - return SIGNATURE_FORMAT % (key_id, sign_header_keys, signature) -- cgit v1.3.1-10-gc9f91