mirror of
https://github.com/Mikaela/Limnoria.git
synced 2025-02-27 19:11:05 +01:00
Fediverse: First draft.
This commit is contained in:
parent
43a8724d3a
commit
e87a14826e
1
plugins/Fediverse/README.md
Normal file
1
plugins/Fediverse/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
Fetches information from ActivityPub servers.
|
72
plugins/Fediverse/__init__.py
Normal file
72
plugins/Fediverse/__init__.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
###
|
||||||
|
# Copyright (c) 2020, Valentin Lorentz
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions, and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions, and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of the author of this software nor the name of
|
||||||
|
# contributors to this software may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written consent.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
"""
|
||||||
|
Fediverse: Fetches information from ActivityPub servers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import supybot
|
||||||
|
from supybot import world
|
||||||
|
|
||||||
|
# Use this for the version of this plugin.
|
||||||
|
__version__ = ""
|
||||||
|
|
||||||
|
# XXX Replace this with an appropriate author or supybot.Author instance.
|
||||||
|
__author__ = supybot.authors.unknown
|
||||||
|
|
||||||
|
# This is a dictionary mapping supybot.Author instances to lists of
|
||||||
|
# contributions.
|
||||||
|
__contributors__ = {}
|
||||||
|
|
||||||
|
# This is a url where the most recent plugin package can be downloaded.
|
||||||
|
__url__ = ""
|
||||||
|
|
||||||
|
from . import config
|
||||||
|
from . import plugin
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 4):
|
||||||
|
from importlib import reload
|
||||||
|
else:
|
||||||
|
from imp import reload
|
||||||
|
# In case we're being reloaded.
|
||||||
|
reload(config)
|
||||||
|
reload(plugin)
|
||||||
|
# Add more reloads here if you add third-party modules and want them to be
|
||||||
|
# reloaded when this plugin is reloaded. Don't forget to import them as well!
|
||||||
|
|
||||||
|
if world.testing:
|
||||||
|
from . import test
|
||||||
|
|
||||||
|
Class = plugin.Class
|
||||||
|
configure = config.configure
|
||||||
|
|
||||||
|
|
||||||
|
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:
|
221
plugins/Fediverse/activitypub.py
Normal file
221
plugins/Fediverse/activitypub.py
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
###
|
||||||
|
# Copyright (c) 2020, Valentin Lorentz
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions, and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions, and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of the author of this software nor the name of
|
||||||
|
# contributors to this software may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written consent.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import functools
|
||||||
|
import contextlib
|
||||||
|
import urllib.parse
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import padding
|
||||||
|
|
||||||
|
|
||||||
|
from supybot import commands, conf
|
||||||
|
from supybot.utils import web
|
||||||
|
|
||||||
|
|
||||||
|
XRD_URI = "{http://docs.oasis-open.org/ns/xri/xrd-1.0}"
|
||||||
|
ACTIVITY_MIMETYPE = "application/activity+json"
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityPubError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ProtocolError(ActivityPubError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HostmetaError(ProtocolError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityPubProtocolError(ActivityPubError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebfingerError(ProtocolError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ActorNotFound(ActivityPubError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def sandbox(f):
|
||||||
|
"""Runs a function in a process with limited memory to prevent
|
||||||
|
XML memory bombs
|
||||||
|
<https://docs.python.org/3/library/xml.html#xml-vulnerabilities>
|
||||||
|
"""
|
||||||
|
|
||||||
|
@functools.wraps(f)
|
||||||
|
def newf(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return commands.process(
|
||||||
|
f,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
timeout=10,
|
||||||
|
heap_size=100 * 1024 * 1024,
|
||||||
|
pn="Fediverse",
|
||||||
|
cn=f.__name__
|
||||||
|
)
|
||||||
|
except (commands.ProcessTimeoutError, MemoryError):
|
||||||
|
raise utils.web.Error(
|
||||||
|
_(
|
||||||
|
"Page is too big or the server took "
|
||||||
|
"too much time to answer the request."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return newf
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def convert_exceptions(to_class, msg="", from_none=False):
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except Exception as e:
|
||||||
|
arg = msg + str(e)
|
||||||
|
if from_none:
|
||||||
|
raise to_class(arg) from None
|
||||||
|
else:
|
||||||
|
raise to_class(arg) from e
|
||||||
|
|
||||||
|
|
||||||
|
@sandbox
|
||||||
|
def _get_webfinger_url(hostname):
|
||||||
|
with convert_exceptions(HostmetaError):
|
||||||
|
doc = ET.fromstring(
|
||||||
|
web.getUrlContent("https://%s/.well-known/host-meta" % hostname)
|
||||||
|
)
|
||||||
|
|
||||||
|
for link in doc.iter(XRD_URI + "Link"):
|
||||||
|
if link.attrib["rel"] == "lrdd":
|
||||||
|
return link.attrib["template"]
|
||||||
|
|
||||||
|
return "https://%s/.well-known/webfinger?resource={uri}"
|
||||||
|
|
||||||
|
|
||||||
|
def webfinger(hostname, uri):
|
||||||
|
template = _get_webfinger_url(hostname)
|
||||||
|
assert template
|
||||||
|
|
||||||
|
with convert_exceptions(WebfingerError):
|
||||||
|
content = web.getUrlContent(
|
||||||
|
template.replace("{uri}", uri),
|
||||||
|
headers={"Accept": "application/json"},
|
||||||
|
)
|
||||||
|
|
||||||
|
with convert_exceptions(WebfingerError, "Invalid JSON: ", True):
|
||||||
|
return json.loads(content)
|
||||||
|
|
||||||
|
|
||||||
|
def get_instance_actor_url():
|
||||||
|
root_url = conf.supybot.servers.http.publicUrl()
|
||||||
|
if not root_url:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return urllib.parse.urljoin(root_url, "/fediverse/instance_actor")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_private_key():
|
||||||
|
path = conf.supybot.directories.data.dirize("Fediverse/instance_key.pem")
|
||||||
|
with open(path, "rb") as fd:
|
||||||
|
return serialization.load_pem_private_key(
|
||||||
|
fd.read(), password=None, backend=default_backend()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_public_key():
|
||||||
|
return _get_private_key().public_key()
|
||||||
|
|
||||||
|
|
||||||
|
def get_public_key_pem():
|
||||||
|
return get_public_key().public_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _signed_request(url, headers, data=None):
|
||||||
|
method = "get" if data is None else "post"
|
||||||
|
instance_actor_url = get_instance_actor_url()
|
||||||
|
|
||||||
|
assert instance_actor_url
|
||||||
|
if instance_actor_url:
|
||||||
|
signed_headers = [
|
||||||
|
(
|
||||||
|
"(request-target)",
|
||||||
|
method + " " + urllib.parse.urlparse(url).path,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
for (header_name, header_value) in headers.items():
|
||||||
|
signed_headers.append((header_name.lower(), header_value))
|
||||||
|
signed_text = "\n".join("%s: %s" % header for header in signed_headers)
|
||||||
|
|
||||||
|
private_key = _get_private_key()
|
||||||
|
signature = private_key.sign(
|
||||||
|
signed_text.encode(), padding.PKCS1v15(), hashes.SHA256()
|
||||||
|
)
|
||||||
|
|
||||||
|
headers["Signature"] = (
|
||||||
|
'keyId="%s#main-key",' % instance_actor_url
|
||||||
|
+ 'headers="%s",' % " ".join(k for (k, v) in signed_headers)
|
||||||
|
+ 'signature="%s"' % base64.b64encode(signature).decode()
|
||||||
|
)
|
||||||
|
|
||||||
|
with convert_exceptions(ActivityPubProtocolError):
|
||||||
|
return web.getUrlContent(url, headers=headers, data=data)
|
||||||
|
|
||||||
|
|
||||||
|
def actor_url(localuser, hostname):
|
||||||
|
uri = "acct:%s@%s" % (localuser, hostname)
|
||||||
|
for link in webfinger(hostname, uri)["links"]:
|
||||||
|
if link["rel"] == "self" and link["type"] == ACTIVITY_MIMETYPE:
|
||||||
|
return link["href"]
|
||||||
|
|
||||||
|
raise ActorNotFound(localuser, hostname)
|
||||||
|
|
||||||
|
|
||||||
|
def get_actor(localuser, hostname):
|
||||||
|
url = actor_url(localuser, hostname)
|
||||||
|
|
||||||
|
content = _signed_request(url, headers={"Accept": ACTIVITY_MIMETYPE})
|
||||||
|
|
||||||
|
assert content is not None
|
||||||
|
|
||||||
|
with convert_exceptions(ActivityPubProtocolError, "Invalid JSON: ", True):
|
||||||
|
return json.loads(content)
|
59
plugins/Fediverse/config.py
Normal file
59
plugins/Fediverse/config.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
###
|
||||||
|
# Copyright (c) 2020, Valentin Lorentz
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions, and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions, and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of the author of this software nor the name of
|
||||||
|
# contributors to this software may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written consent.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
from supybot import conf, registry
|
||||||
|
|
||||||
|
try:
|
||||||
|
from supybot.i18n import PluginInternationalization
|
||||||
|
|
||||||
|
_ = PluginInternationalization("Fediverse")
|
||||||
|
except:
|
||||||
|
# Placeholder that allows to run the plugin on a bot
|
||||||
|
# without the i18n module
|
||||||
|
_ = lambda x: x
|
||||||
|
|
||||||
|
|
||||||
|
def configure(advanced):
|
||||||
|
# This will be called by supybot to configure this module. advanced is
|
||||||
|
# a bool that specifies whether the user identified themself as an advanced
|
||||||
|
# user or not. You should effect your configuration by manipulating the
|
||||||
|
# registry as appropriate.
|
||||||
|
from supybot.questions import expect, anything, something, yn
|
||||||
|
|
||||||
|
conf.registerPlugin("Fediverse", True)
|
||||||
|
|
||||||
|
|
||||||
|
Fediverse = conf.registerPlugin("Fediverse")
|
||||||
|
# This is where your configuration variables (if any) should go. For example:
|
||||||
|
# conf.registerGlobalValue(Fediverse, 'someConfigVariableName',
|
||||||
|
# registry.Boolean(False, _("""Help for someConfigVariableName.""")))
|
||||||
|
|
||||||
|
|
||||||
|
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:
|
1
plugins/Fediverse/local/__init__.py
Normal file
1
plugins/Fediverse/local/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Stub so local is a module, used for third-party modules
|
204
plugins/Fediverse/plugin.py
Normal file
204
plugins/Fediverse/plugin.py
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
###
|
||||||
|
# Copyright (c) 2020, Valentin Lorentz
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions, and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions, and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of the author of this software nor the name of
|
||||||
|
# contributors to this software may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written consent.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import importlib
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
from supybot import conf, utils, plugins, ircutils, callbacks, httpserver
|
||||||
|
from supybot.commands import *
|
||||||
|
from supybot.i18n import PluginInternationalization
|
||||||
|
|
||||||
|
_ = PluginInternationalization("Fediverse")
|
||||||
|
|
||||||
|
from . import activitypub as ap
|
||||||
|
|
||||||
|
importlib.reload(ap)
|
||||||
|
|
||||||
|
|
||||||
|
_username_re = re.compile("@(?P<localuser>[^@]+)@(?P<hostname>[^@]+)")
|
||||||
|
|
||||||
|
|
||||||
|
class FediverseHttp(httpserver.SupyHTTPServerCallback):
|
||||||
|
name = "minimal ActivityPub server"
|
||||||
|
defaultResponse = _(
|
||||||
|
"""
|
||||||
|
You shouldn't be here, this subfolder is not for you. Go back to the
|
||||||
|
index and try out other plugins (if any)."""
|
||||||
|
)
|
||||||
|
|
||||||
|
def doGetOrHead(self, handler, path, write_content):
|
||||||
|
if path == "/instance_actor":
|
||||||
|
self.instance_actor(write_content)
|
||||||
|
else:
|
||||||
|
assert False, repr(path)
|
||||||
|
|
||||||
|
def doWellKnown(self, handler, path):
|
||||||
|
actor_url = ap.get_instance_actor_url()
|
||||||
|
instance_hostname = urllib.parse.urlsplit(actor_url).hostname
|
||||||
|
instance_account = "acct:%s@%s" % (
|
||||||
|
instance_hostname,
|
||||||
|
instance_hostname,
|
||||||
|
)
|
||||||
|
if path == "/webfinger?resource=%s" % instance_account:
|
||||||
|
headers = {"Content-Type": "application/jrd+json"}
|
||||||
|
content = {
|
||||||
|
"subject": instance_account,
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"rel": "self",
|
||||||
|
"type": "application/activity+json",
|
||||||
|
"href": actor_url,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
return (200, headers, json.dumps(content).encode())
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def instance_actor(self, write_content):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-type", ap.ACTIVITY_MIMETYPE)
|
||||||
|
self.end_headers()
|
||||||
|
if not write_content:
|
||||||
|
return
|
||||||
|
pem = ap.get_public_key_pem()
|
||||||
|
actor_url = ap.get_instance_actor_url()
|
||||||
|
hostname = urllib.parse.urlparse(hostname).hostname
|
||||||
|
actor = {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
"https://w3id.org/security/v1",
|
||||||
|
],
|
||||||
|
"id": actor_url,
|
||||||
|
"preferredUsername": hostname,
|
||||||
|
"type": "Person",
|
||||||
|
"publicKey": {
|
||||||
|
"id": actor_url + "#main-key",
|
||||||
|
"owner": actor_url,
|
||||||
|
"publicKeyPem": pem.decode(),
|
||||||
|
},
|
||||||
|
"inbox": actor_url + "/inbox",
|
||||||
|
}
|
||||||
|
self.wfile.write(json.dumps(actor).encode())
|
||||||
|
|
||||||
|
|
||||||
|
class Fediverse(callbacks.Plugin):
|
||||||
|
"""Fetches information from ActivityPub servers."""
|
||||||
|
|
||||||
|
threaded = True
|
||||||
|
|
||||||
|
def __init__(self, irc):
|
||||||
|
super().__init__(irc)
|
||||||
|
self._startHttp()
|
||||||
|
|
||||||
|
def _startHttp(self):
|
||||||
|
callback = FediverseHttp()
|
||||||
|
callback._plugin = self
|
||||||
|
httpserver.hook("fediverse", callback)
|
||||||
|
|
||||||
|
def die(self):
|
||||||
|
self._stopHttp()
|
||||||
|
super().die()
|
||||||
|
|
||||||
|
def _stopHttp(self):
|
||||||
|
httpserver.unhook("fediverse")
|
||||||
|
|
||||||
|
@wrap(["somethingWithoutSpaces"])
|
||||||
|
def profile(self, irc, msg, args, username):
|
||||||
|
"""<@user@instance>
|
||||||
|
|
||||||
|
Returns generic information on the account @user@instance."""
|
||||||
|
match = _username_re.match(username)
|
||||||
|
if not match:
|
||||||
|
irc.errorInvalid("fediverse username", username)
|
||||||
|
localuser = match.group("localuser")
|
||||||
|
hostname = match.group("hostname")
|
||||||
|
|
||||||
|
try:
|
||||||
|
actor = ap.get_actor(localuser, hostname)
|
||||||
|
except ap.WebfingerError as e:
|
||||||
|
# Usually a 404
|
||||||
|
irc.error(e.args[0], Raise=True)
|
||||||
|
|
||||||
|
irc.reply(
|
||||||
|
_("\x02%s\x02 (@%s@%s): %s")
|
||||||
|
% (
|
||||||
|
actor["name"],
|
||||||
|
actor["preferredUsername"],
|
||||||
|
hostname,
|
||||||
|
utils.web.htmlToText(actor["summary"], tagReplace=""),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
'''
|
||||||
|
@wrap(['somethingWithoutSpaces'])
|
||||||
|
def post(self, irc, msg, args, username):
|
||||||
|
"""<@user@instance>
|
||||||
|
|
||||||
|
Returns generic information on the account @user@instance."""
|
||||||
|
match = _username_re.match(username)
|
||||||
|
if not match:
|
||||||
|
irc.errorInvalid('fediverse username', username)
|
||||||
|
localuser = match.group('localuser')
|
||||||
|
hostname = match.group('hostname')
|
||||||
|
|
||||||
|
instance_actor = ap.get_instance_actor_url()
|
||||||
|
instance_hostname = urllib.parse.urlparse(
|
||||||
|
conf.supybot.servers.http.publicUrl()).hostname
|
||||||
|
doc = {
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
|
||||||
|
"id": "https://%s/create-hello-world" % instance_hostname,
|
||||||
|
"type": "Create",
|
||||||
|
"actor": instance_actor,
|
||||||
|
|
||||||
|
"object": {
|
||||||
|
"id": "https://%s/hello-world" % instance_hostname,
|
||||||
|
"type": "Note",
|
||||||
|
"published": "2018-06-23T17:17:11Z",
|
||||||
|
"attributedTo": instance_actor,
|
||||||
|
"content": "<p>Hello world</p>",
|
||||||
|
"to": "https://www.w3.org/ns/activitystreams#Public"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ap._signed_request(
|
||||||
|
url='https://%s/inbox' % hostname,
|
||||||
|
headers={},
|
||||||
|
data=json.dumps(doc),
|
||||||
|
)'''
|
||||||
|
|
||||||
|
|
||||||
|
Class = Fediverse
|
||||||
|
|
||||||
|
|
||||||
|
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
|
90
plugins/Fediverse/test.py
Normal file
90
plugins/Fediverse/test.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
###
|
||||||
|
# Copyright (c) 2020, Valentin Lorentz
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions, and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions, and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of the author of this software nor the name of
|
||||||
|
# contributors to this software may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written consent.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
from supybot import conf
|
||||||
|
from supybot.test import *
|
||||||
|
|
||||||
|
|
||||||
|
PRIVATE_KEY = b"""
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEpAIBAAKCAQEA6jtjTlaTh1aR+q3gpZvb4dj8s81zKmwy7cwn44LtLV+ivNf/
|
||||||
|
SkWPr1zkm/gWFItC3058Faqk9p4fdJaxVJJTW0KL7LlJs+LTcMsLi2nTgvBZg7oE
|
||||||
|
KRXZxuJJcc5QNkgY8vHt1PxdD17mZBGwfg2loZfnjZOOz4F8wdQ18Da1ZFUFyc+R
|
||||||
|
qj1THdXbpBjF7zcNyJOWzzRwhpiqdJomnTAYDscAkkF2/gI8tYP+Is31GOE1phPC
|
||||||
|
DH20uvJNUtDnXSdUm2Ol21LmePV4pWS75mcIHz5YAKwAGo9XoUQa8lC6IHw6LX+y
|
||||||
|
CVKkoSc0Ouzr3acQCLZ8EDUIh2nMhw/VtYV7JwIDAQABAoIBAFSARkwtqZ1qmtFf
|
||||||
|
xyqXttScblYDaWfFjv4A5+cJBb2XweL03ZGS1MpD7elir7yLnP1omBVM8aRS2TA7
|
||||||
|
aRAElfPXZxloovE1hGgtqCWMcRTM1s5R3kxgKKe6XRqkfoWGrxF+O/nZbU0tRFqX
|
||||||
|
kx92lulcHtoRgLTVlwdqImddpUTjQrWmrt3nEjTZj5tHcPGdC2ovH/bFrganbCR1
|
||||||
|
If6xG2r6RWSfMEpj7yFTKRvnLCr2VpviDOwFh/zZdwyqBRKW6LNZP04TtlFfKh5C
|
||||||
|
1R2tZVRHQ7Ed99yruirW0rmgOjA6dJTpN6oX6x3DpTi48oK2jktEIk07P7jy1mZY
|
||||||
|
NeCQcqkCgYEA+M0DQ+0fBm/RJyDIxsupMAf8De1kG6Bj8gVSRnvtD0Fb3LTswT3I
|
||||||
|
TDnIVttjOzBsbpZVdjdCE9Wcfj9pIwu3YTO54OTS8kiwYRshzEm3UpdPOSCnIZUx
|
||||||
|
jwbbwEHq0zEeIWVjDBDXN2fqEcu7gFqBzYivAh8hYq78BJkUeBWU3N0CgYEA8QJ0
|
||||||
|
6xS551VEGLbn9h5pPxze7l8a9WJc1uIxRexeBtd4UwJ5e1yLN68FVNjGr3JtreJ3
|
||||||
|
KP/VyynFubNRvwGEnifKe9QyiATFCbVeAFBQFuA0w89LOmBiHc+uHz1uA5oXnD99
|
||||||
|
Y0pEu8g+QsBKeQowMhkYnw4h5cq3AVCKRIdNpdMCgYEAwy5p8l7SKQWNagnBGJtr
|
||||||
|
BeAtr2tdToL8BUCBdAQCTCZ0/2b8GPjz6kCmVuVTKnrphbPwJYZiExdP5oauXyzw
|
||||||
|
1pNyreg1SJcXr4ZOdGocI/HJ18Iy+xiEwXSa7m+H3dg5j+9uzWdkvvWJXh6a4K2g
|
||||||
|
CPLCgIKVeUpXMPA6a55aow0CgYAMpoRckonvipo4ceFbGd2MYoeRG4zetHsLDHRp
|
||||||
|
py6ITWcTdF3MC9+C3Lz65yYGr4ryRaDblhIyx86JINB5piq/4nbOaST93sI48Dwu
|
||||||
|
6AhMKxiZ7peUSNrdlbkeCqtrpPr4SJzcSVmyQaCDAHToRZCiEI8qSiOdXDae6wtW
|
||||||
|
7YM14QKBgQDnbseQK0yzrsZoOmQ9PBULr4vNLiL5+OllOG1+GNNztk/Q+Xfx6Hvw
|
||||||
|
h6cgTcpZsvaa2CW6A2yqenmGfKBgiRoN39vFqjVDkjL1HaL3rPeK1H7RWrz1Sto7
|
||||||
|
rut+UhYHat9fo6950Wvxa4Iee9q0NOF0HUkD6WupcPyC0nSEex8Z6A==
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class FediverseTestCase(PluginTestCase):
|
||||||
|
plugins = ("Fediverse",)
|
||||||
|
config = {
|
||||||
|
"supybot.servers.http.port": 18080,
|
||||||
|
"supybot.servers.http.publicUrl": "https://particle18080.test.progval.net/",
|
||||||
|
}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
path = conf.supybot.directories.data.dirize(
|
||||||
|
"Fediverse/instance_key.pem"
|
||||||
|
)
|
||||||
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
with open(path, "wb") as fd:
|
||||||
|
fd.write(PRIVATE_KEY)
|
||||||
|
|
||||||
|
if network:
|
||||||
|
|
||||||
|
def testProfile(self):
|
||||||
|
self.assertRegexp("profile @val@oc.todon.fr", "0E082B40E4376B1E")
|
||||||
|
# TODO: add a test with an instance which only allows fetches
|
||||||
|
# with valid signatures.
|
||||||
|
|
||||||
|
|
||||||
|
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:
|
@ -1388,6 +1388,11 @@ registerGlobalValue(supybot.servers.http, 'keepAlive',
|
|||||||
registerGlobalValue(supybot.servers.http, 'favicon',
|
registerGlobalValue(supybot.servers.http, 'favicon',
|
||||||
registry.String('', _("""Determines the path of the file served as
|
registry.String('', _("""Determines the path of the file served as
|
||||||
favicon to browsers.""")))
|
favicon to browsers.""")))
|
||||||
|
registerGlobalValue(supybot.servers.http, 'publicUrl',
|
||||||
|
registry.String('', _("""Determines the public URL of the server.
|
||||||
|
By default it is http://<hostname>:<port>/, but you will want to change
|
||||||
|
this if there is a reverse proxy (nginx, apache, ...) in front of
|
||||||
|
the bot.""")))
|
||||||
|
|
||||||
|
|
||||||
###
|
###
|
||||||
|
@ -178,7 +178,7 @@ class RealSupyHTTPServer(HTTPServer):
|
|||||||
else:
|
else:
|
||||||
raise AssertionError(protocol)
|
raise AssertionError(protocol)
|
||||||
HTTPServer.__init__(self, address, callback)
|
HTTPServer.__init__(self, address, callback)
|
||||||
self.callbacks = {}
|
self.callbacks = DEFAULT_CALLBACKS
|
||||||
|
|
||||||
def server_bind(self):
|
def server_bind(self):
|
||||||
if self.protocol == 6:
|
if self.protocol == 6:
|
||||||
@ -322,6 +322,10 @@ class SupyHTTPServerCallback(log.Firewalled):
|
|||||||
|
|
||||||
doPost = doGet
|
doPost = doGet
|
||||||
|
|
||||||
|
def doWellKnown(self, handler, path):
|
||||||
|
"""Handles GET request to /.well-known/"""
|
||||||
|
return None
|
||||||
|
|
||||||
def doHook(self, handler, subdir):
|
def doHook(self, handler, subdir):
|
||||||
"""Method called when hooking this callback."""
|
"""Method called when hooking this callback."""
|
||||||
pass
|
pass
|
||||||
@ -352,7 +356,6 @@ class Supy404(SupyHTTPServerCallback):
|
|||||||
class SupyIndex(SupyHTTPServerCallback):
|
class SupyIndex(SupyHTTPServerCallback):
|
||||||
"""Displays the index of available plugins."""
|
"""Displays the index of available plugins."""
|
||||||
name = "index"
|
name = "index"
|
||||||
fullpath = True
|
|
||||||
defaultResponse = _("Request not handled.")
|
defaultResponse = _("Request not handled.")
|
||||||
def doGetOrHead(self, handler, path, write_content):
|
def doGetOrHead(self, handler, path, write_content):
|
||||||
plugins = [x for x in handler.server.callbacks.items()]
|
plugins = [x for x in handler.server.callbacks.items()]
|
||||||
@ -428,6 +431,27 @@ class Favicon(SupyHTTPServerCallback):
|
|||||||
if write_content:
|
if write_content:
|
||||||
self.wfile.write(response)
|
self.wfile.write(response)
|
||||||
|
|
||||||
|
class SupyWellKnown(SupyHTTPServerCallback):
|
||||||
|
"""Serves /.well-known/ resources."""
|
||||||
|
name = 'well-known'
|
||||||
|
defaultResponse = _('Request not handled')
|
||||||
|
|
||||||
|
def doGetOrHead(self, handler, path, write_content):
|
||||||
|
for callback in handler.server.callbacks.values():
|
||||||
|
resp = callback.doWellKnown(handler, path)
|
||||||
|
if resp:
|
||||||
|
(status, headers, content) = resp
|
||||||
|
handler.send_response(status)
|
||||||
|
for header in headers.items():
|
||||||
|
self.send_header(*header)
|
||||||
|
self.end_headers()
|
||||||
|
if write_content:
|
||||||
|
self.wfile.write(content)
|
||||||
|
return
|
||||||
|
|
||||||
|
handler.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
http_servers = []
|
http_servers = []
|
||||||
|
|
||||||
def startServer():
|
def startServer():
|
||||||
@ -471,6 +495,9 @@ def unhook(subdir):
|
|||||||
assert isinstance(http_servers, list)
|
assert isinstance(http_servers, list)
|
||||||
for server in list(http_servers):
|
for server in list(http_servers):
|
||||||
server.unhook(subdir)
|
server.unhook(subdir)
|
||||||
if len(server.callbacks) <= 0 and not configGroup.keepAlive():
|
if len(set(server.callbacks) - set(DEFAULT_CALLBACKS)) <= 0 \
|
||||||
|
and not configGroup.keepAlive():
|
||||||
server.shutdown()
|
server.shutdown()
|
||||||
http_servers.remove(server)
|
http_servers.remove(server)
|
||||||
|
|
||||||
|
DEFAULT_CALLBACKS = {'.well-known': SupyWellKnown()}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user