mirror of
https://github.com/Mikaela/Limnoria.git
synced 2024-11-26 12:49:24 +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',
|
||||
registry.String('', _("""Determines the path of the file served as
|
||||
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:
|
||||
raise AssertionError(protocol)
|
||||
HTTPServer.__init__(self, address, callback)
|
||||
self.callbacks = {}
|
||||
self.callbacks = DEFAULT_CALLBACKS
|
||||
|
||||
def server_bind(self):
|
||||
if self.protocol == 6:
|
||||
@ -322,6 +322,10 @@ class SupyHTTPServerCallback(log.Firewalled):
|
||||
|
||||
doPost = doGet
|
||||
|
||||
def doWellKnown(self, handler, path):
|
||||
"""Handles GET request to /.well-known/"""
|
||||
return None
|
||||
|
||||
def doHook(self, handler, subdir):
|
||||
"""Method called when hooking this callback."""
|
||||
pass
|
||||
@ -352,7 +356,6 @@ class Supy404(SupyHTTPServerCallback):
|
||||
class SupyIndex(SupyHTTPServerCallback):
|
||||
"""Displays the index of available plugins."""
|
||||
name = "index"
|
||||
fullpath = True
|
||||
defaultResponse = _("Request not handled.")
|
||||
def doGetOrHead(self, handler, path, write_content):
|
||||
plugins = [x for x in handler.server.callbacks.items()]
|
||||
@ -428,6 +431,27 @@ class Favicon(SupyHTTPServerCallback):
|
||||
if write_content:
|
||||
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 = []
|
||||
|
||||
def startServer():
|
||||
@ -471,6 +495,9 @@ def unhook(subdir):
|
||||
assert isinstance(http_servers, list)
|
||||
for server in list(http_servers):
|
||||
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()
|
||||
http_servers.remove(server)
|
||||
|
||||
DEFAULT_CALLBACKS = {'.well-known': SupyWellKnown()}
|
||||
|
Loading…
Reference in New Issue
Block a user