2022-12-26 05:09:34 +00:00
|
|
|
"""Simple conneg endpoint that serves AS2 or redirects to to the original post.
|
2018-10-14 14:42:28 +00:00
|
|
|
|
2023-10-06 06:32:31 +00:00
|
|
|
Only for :class:`web.Web` users. Other protocols (including :class:`web.Web`
|
2023-10-06 15:22:50 +00:00
|
|
|
sometimes) use ``/convert/`` in convert.py instead.
|
2023-06-10 22:07:26 +00:00
|
|
|
|
2023-10-06 06:32:31 +00:00
|
|
|
Serves ``/r/https://foo.com/bar`` URL paths, where ``https://foo.com/bar`` is a
|
|
|
|
original post for a :class:`Web` user. Needed for Mastodon interop, they require
|
|
|
|
that AS2 object ids and urls are on the same domain that serves them.
|
|
|
|
Background:
|
2018-10-14 14:42:28 +00:00
|
|
|
|
2023-10-06 06:32:31 +00:00
|
|
|
* https://github.com/snarfed/bridgy-fed/issues/16#issuecomment-424799599
|
|
|
|
* https://github.com/tootsuite/mastodon/pull/6219#issuecomment-429142747
|
2022-12-26 05:09:34 +00:00
|
|
|
|
2023-10-06 06:32:31 +00:00
|
|
|
The conneg makes these ``/r/`` URLs searchable in Mastodon:
|
2022-12-26 05:09:34 +00:00
|
|
|
https://github.com/snarfed/bridgy-fed/issues/352
|
2018-10-14 14:42:28 +00:00
|
|
|
"""
|
2019-04-16 15:02:19 +00:00
|
|
|
import logging
|
2021-06-29 05:52:04 +00:00
|
|
|
import re
|
2021-03-07 15:36:34 +00:00
|
|
|
import urllib.parse
|
2018-10-14 14:42:28 +00:00
|
|
|
|
2024-06-04 21:19:04 +00:00
|
|
|
from flask import redirect, request
|
2023-02-12 02:35:34 +00:00
|
|
|
from granary import as2
|
2022-12-26 21:34:50 +00:00
|
|
|
from negotiator import ContentNegotiator, AcceptParameters, ContentType
|
2021-07-18 04:22:13 +00:00
|
|
|
from oauth_dropins.webutil import flask_util, util
|
2021-08-06 17:29:25 +00:00
|
|
|
from oauth_dropins.webutil.flask_util import error
|
2023-02-12 02:35:34 +00:00
|
|
|
from oauth_dropins.webutil.util import json_dumps, json_loads
|
2018-10-14 14:42:28 +00:00
|
|
|
|
2023-05-24 04:30:57 +00:00
|
|
|
from activitypub import ActivityPub
|
2024-06-04 21:19:04 +00:00
|
|
|
from common import CACHE_CONTROL, CONTENT_TYPE_HTML
|
2024-05-30 22:12:19 +00:00
|
|
|
from flask_app import app
|
2024-04-15 01:26:34 +00:00
|
|
|
from protocol import Protocol
|
2023-05-27 00:40:29 +00:00
|
|
|
from web import Web
|
2018-10-14 14:42:28 +00:00
|
|
|
|
2022-02-12 06:38:56 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2022-12-26 05:09:34 +00:00
|
|
|
_negotiator = ContentNegotiator(acceptable=[
|
|
|
|
AcceptParameters(ContentType(CONTENT_TYPE_HTML)),
|
2023-01-07 05:01:33 +00:00
|
|
|
AcceptParameters(ContentType(as2.CONTENT_TYPE)),
|
|
|
|
AcceptParameters(ContentType(as2.CONTENT_TYPE_LD)),
|
2022-12-26 05:09:34 +00:00
|
|
|
])
|
|
|
|
|
2023-09-23 20:50:19 +00:00
|
|
|
DOMAIN_ALLOWLIST = frozenset((
|
|
|
|
'bsky.app',
|
|
|
|
))
|
2019-04-16 15:02:19 +00:00
|
|
|
|
2024-05-30 22:12:19 +00:00
|
|
|
HEADERS = {
|
|
|
|
'Vary': 'Accept',
|
2024-06-04 21:19:04 +00:00
|
|
|
**CACHE_CONTROL,
|
2024-05-30 22:12:19 +00:00
|
|
|
}
|
2023-12-31 17:20:56 +00:00
|
|
|
|
2021-07-11 23:30:14 +00:00
|
|
|
@app.get(r'/r/<path:to>')
|
2024-06-04 21:19:04 +00:00
|
|
|
@flask_util.headers(HEADERS)
|
2021-08-06 17:28:56 +00:00
|
|
|
def redir(to):
|
2023-05-22 22:27:43 +00:00
|
|
|
"""Either redirect to a given URL or convert it to another format.
|
2018-10-14 14:42:28 +00:00
|
|
|
|
2023-10-06 06:32:31 +00:00
|
|
|
E.g. redirects ``/r/https://foo.com/bar?baz`` to
|
|
|
|
``https://foo.com/bar?baz``, or if it's requested with AS2 conneg in the
|
|
|
|
``Accept`` header, fetches and converts and serves it as AS2.
|
2018-10-14 14:42:28 +00:00
|
|
|
"""
|
2021-07-08 04:02:13 +00:00
|
|
|
if request.args:
|
|
|
|
to += '?' + urllib.parse.urlencode(request.args)
|
|
|
|
# some browsers collapse repeated /s in the path down to a single slash.
|
|
|
|
# if that happened to this URL, expand it back to two /s.
|
|
|
|
to = re.sub(r'^(https?:/)([^/])', r'\1/\2', to)
|
|
|
|
|
2022-12-10 17:04:05 +00:00
|
|
|
if not util.is_web(to):
|
2021-08-06 17:29:25 +00:00
|
|
|
error(f'Expected fully qualified URL; got {to}')
|
2021-07-08 04:02:13 +00:00
|
|
|
|
2023-10-18 18:02:50 +00:00
|
|
|
try:
|
|
|
|
to_domain = urllib.parse.urlparse(to).hostname
|
|
|
|
except ValueError as e:
|
|
|
|
error(f'Invalid URL {to} : {e}')
|
2023-05-22 22:27:43 +00:00
|
|
|
|
|
|
|
# check conneg
|
|
|
|
accept_as2 = False
|
|
|
|
accept = request.headers.get('Accept')
|
|
|
|
if accept:
|
|
|
|
try:
|
|
|
|
negotiated = _negotiator.negotiate(accept)
|
|
|
|
except ValueError:
|
|
|
|
# work around https://github.com/CottageLabs/negotiator/issues/6
|
|
|
|
negotiated = None
|
|
|
|
if negotiated:
|
|
|
|
accept_type = str(negotiated.content_type)
|
|
|
|
if accept_type in (as2.CONTENT_TYPE, as2.CONTENT_TYPE_LD):
|
|
|
|
accept_as2 = True
|
|
|
|
|
2021-07-08 04:02:13 +00:00
|
|
|
# check that we've seen this domain before so we're not an open redirect
|
2022-11-08 00:26:33 +00:00
|
|
|
domains = set((util.domain_from_link(to, minimize=True),
|
|
|
|
util.domain_from_link(to, minimize=False),
|
2023-05-22 22:27:43 +00:00
|
|
|
to_domain))
|
2024-04-15 01:26:34 +00:00
|
|
|
web_user = None
|
2021-07-08 04:02:13 +00:00
|
|
|
for domain in domains:
|
2022-11-16 05:37:39 +00:00
|
|
|
if domain:
|
2023-09-23 20:50:19 +00:00
|
|
|
if domain in DOMAIN_ALLOWLIST:
|
|
|
|
break
|
2024-04-15 01:26:34 +00:00
|
|
|
if web_user := Web.get_by_id(domain):
|
2023-05-26 23:07:36 +00:00
|
|
|
logger.info(f'Found web user for domain {domain}')
|
2022-11-16 05:37:39 +00:00
|
|
|
break
|
2021-07-08 04:02:13 +00:00
|
|
|
else:
|
2023-06-05 03:58:21 +00:00
|
|
|
if not accept_as2:
|
2024-06-04 21:19:04 +00:00
|
|
|
return f'No web user found for any of {domains}', 404
|
2021-07-08 04:02:13 +00:00
|
|
|
|
2024-04-15 01:26:34 +00:00
|
|
|
if not accept_as2:
|
|
|
|
# redirect. include rel-alternate link to make posts discoverable by entering
|
|
|
|
# https://fed.brid.gy/r/[URL] in a fediverse instance's search.
|
|
|
|
logger.info(f'redirecting to {to}')
|
|
|
|
return f"""\
|
|
|
|
<!doctype html>
|
|
|
|
<html>
|
|
|
|
<head>
|
|
|
|
<link href="{request.url}" rel="alternate" type="application/activity+json">
|
|
|
|
</head>
|
|
|
|
<title>Redirecting...</title>
|
|
|
|
<h1>Redirecting...</h1>
|
|
|
|
<p>You should be redirected automatically to the target URL: <a href="{to}">{to}</a>. If not, click the link.
|
|
|
|
</html>
|
2024-06-04 21:19:04 +00:00
|
|
|
""", 301, {'Location': to}
|
2024-04-15 01:26:34 +00:00
|
|
|
|
|
|
|
# AS2 requested, fetch and convert and serve
|
|
|
|
proto = Protocol.for_id(to)
|
|
|
|
if not proto:
|
2024-06-04 21:19:04 +00:00
|
|
|
return f"Couldn't determine protocol for {to}", 404
|
2024-04-15 01:26:34 +00:00
|
|
|
|
|
|
|
obj = proto.load(to)
|
|
|
|
if not obj or obj.deleted:
|
2024-06-04 21:19:04 +00:00
|
|
|
return f'Object not found: {to}', 404
|
2024-04-15 01:26:34 +00:00
|
|
|
|
|
|
|
# TODO: do this for other protocols too?
|
|
|
|
if proto == Web and not web_user:
|
|
|
|
web_user = Web.get_or_create(util.domain_from_link(to), direct=False, obj=obj)
|
|
|
|
if not web_user:
|
2024-06-04 21:19:04 +00:00
|
|
|
return f'Object not found: {to}', 404
|
2023-06-05 03:58:21 +00:00
|
|
|
|
2024-04-15 01:26:34 +00:00
|
|
|
ret = ActivityPub.convert(obj, from_user=web_user)
|
2024-05-30 21:55:35 +00:00
|
|
|
# logger.info(f'Returning: {json_dumps(ret, indent=2)}')
|
2024-04-15 01:26:34 +00:00
|
|
|
return ret, {
|
|
|
|
'Content-Type': (as2.CONTENT_TYPE_LD_PROFILE
|
|
|
|
if accept_type == as2.CONTENT_TYPE_LD
|
|
|
|
else accept_type),
|
|
|
|
'Access-Control-Allow-Origin': '*',
|
|
|
|
}
|
2024-01-28 16:54:35 +00:00
|
|
|
|