kopia lustrzana https://github.com/snarfed/bridgy-fed
113 wiersze
3.7 KiB
Python
113 wiersze
3.7 KiB
Python
"""Simple conneg endpoint that serves AS2 or redirects to to the original post.
|
|
|
|
Serves /r/https://foo.com/bar URL paths, where https://foo.com/bar is an
|
|
original post. Needed for Mastodon interop, they require that AS2 object ids and
|
|
urls are on the same domain that serves them. Background:
|
|
|
|
https://github.com/snarfed/bridgy-fed/issues/16#issuecomment-424799599
|
|
https://github.com/tootsuite/mastodon/pull/6219#issuecomment-429142747
|
|
|
|
The conneg makes these /r/ URLs searchable in Mastodon:
|
|
https://github.com/snarfed/bridgy-fed/issues/352
|
|
"""
|
|
import datetime
|
|
import logging
|
|
import re
|
|
import urllib.parse
|
|
|
|
from flask import redirect, request
|
|
from granary import as2, microformats2
|
|
import mf2util
|
|
from negotiator import ContentNegotiator, AcceptParameters, ContentType
|
|
from oauth_dropins.webutil import flask_util, util
|
|
from oauth_dropins.webutil.flask_util import error
|
|
from oauth_dropins.webutil.util import json_dumps
|
|
from werkzeug.exceptions import abort
|
|
|
|
from app import app, cache
|
|
from common import (
|
|
CACHE_TIME,
|
|
CONTENT_TYPE_HTML,
|
|
postprocess_as2,
|
|
)
|
|
from models import User
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_negotiator = ContentNegotiator(acceptable=[
|
|
AcceptParameters(ContentType(CONTENT_TYPE_HTML)),
|
|
AcceptParameters(ContentType(as2.CONTENT_TYPE)),
|
|
AcceptParameters(ContentType(as2.CONTENT_TYPE_LD)),
|
|
])
|
|
|
|
|
|
@app.get(r'/r/<path:to>')
|
|
@flask_util.cached(cache, CACHE_TIME, headers=['Accept'], http_5xx=True)
|
|
def redir(to):
|
|
"""301 redirect to the embedded fully qualified URL.
|
|
|
|
e.g. redirects /r/https://foo.com/bar?baz to https://foo.com/bar?baz
|
|
"""
|
|
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)
|
|
|
|
if not util.is_web(to):
|
|
error(f'Expected fully qualified URL; got {to}')
|
|
|
|
# check that we've seen this domain before so we're not an open redirect
|
|
domains = set((util.domain_from_link(to, minimize=True),
|
|
util.domain_from_link(to, minimize=False),
|
|
urllib.parse.urlparse(to).hostname))
|
|
for domain in domains:
|
|
if domain:
|
|
user = User.get_by_id(domain)
|
|
if user:
|
|
logger.info(f'Found User for domain {domain}')
|
|
break
|
|
else:
|
|
logger.info(f'No user found for any of {domains}; returning 404')
|
|
abort(404)
|
|
|
|
# check conneg, serve AS2 if requested
|
|
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:
|
|
type = str(negotiated.content_type)
|
|
if type in (as2.CONTENT_TYPE, as2.CONTENT_TYPE_LD):
|
|
return convert_to_as2(to, user), {
|
|
'Content-Type': type,
|
|
'Access-Control-Allow-Origin': '*',
|
|
}
|
|
|
|
# redirect
|
|
logger.info(f'redirecting to {to}')
|
|
return redirect(to, code=301)
|
|
|
|
|
|
def convert_to_as2(url, domain):
|
|
"""Fetch a URL as HTML, convert it to AS2, and return it.
|
|
|
|
Args:
|
|
url: str
|
|
domain: :class:`User`
|
|
|
|
Returns:
|
|
dict: AS2 object
|
|
"""
|
|
mf2 = util.fetch_mf2(url)
|
|
entry = mf2util.find_first_entry(mf2, ['h-entry'])
|
|
logger.info(f"Parsed mf2 for {mf2['url']}: {json_dumps(entry, indent=2)}")
|
|
|
|
obj = postprocess_as2(as2.from_as1(microformats2.json_to_object(entry)),
|
|
domain, create=False)
|
|
logger.info(f'Returning: {json_dumps(obj, indent=2)}')
|
|
return obj
|