2017-08-19 15:21:05 +00:00
|
|
|
"""Handles requests for WebFinger endpoints.
|
|
|
|
|
|
|
|
https://webfinger.net/
|
2017-08-27 18:53:48 +00:00
|
|
|
https://tools.ietf.org/html/rfc7033
|
2017-08-19 15:21:05 +00:00
|
|
|
"""
|
2019-04-16 15:02:19 +00:00
|
|
|
import datetime
|
2017-08-19 15:21:05 +00:00
|
|
|
import logging
|
2021-07-11 20:39:19 +00:00
|
|
|
import re
|
2019-12-26 06:20:57 +00:00
|
|
|
import urllib.parse
|
2017-08-19 15:21:05 +00:00
|
|
|
|
2023-03-20 21:28:14 +00:00
|
|
|
from flask import g, render_template, request
|
2023-02-10 04:00:58 +00:00
|
|
|
from granary import as2
|
2021-08-16 18:47:31 +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-10 04:00:58 +00:00
|
|
|
from oauth_dropins.webutil.util import json_dumps, json_loads
|
2017-08-19 15:21:05 +00:00
|
|
|
|
2021-07-11 20:39:19 +00:00
|
|
|
from app import app, cache
|
2017-08-19 15:21:05 +00:00
|
|
|
import common
|
2022-11-16 06:00:28 +00:00
|
|
|
from models import User
|
2017-08-19 15:21:05 +00:00
|
|
|
|
2019-04-19 14:59:44 +00:00
|
|
|
NON_TLDS = frozenset(('html', 'json', 'php', 'xml'))
|
2019-04-16 15:02:19 +00:00
|
|
|
|
2022-02-12 06:38:56 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2017-08-19 15:21:05 +00:00
|
|
|
|
2022-11-16 06:00:28 +00:00
|
|
|
class Actor(flask_util.XrdOrJrd):
|
2023-02-12 02:59:19 +00:00
|
|
|
"""Serves a user's WebFinger profile."""
|
2023-02-10 04:00:58 +00:00
|
|
|
@flask_util.cached(cache, common.CACHE_TIME, headers=['Accept'])
|
2022-12-26 21:34:50 +00:00
|
|
|
def dispatch_request(self, *args, **kwargs):
|
|
|
|
return super().dispatch_request(*args, **kwargs)
|
|
|
|
|
2021-07-11 23:30:14 +00:00
|
|
|
def template_prefix(self):
|
|
|
|
return 'webfinger_user'
|
|
|
|
|
2023-02-12 02:59:19 +00:00
|
|
|
def template_vars(self, domain=None):
|
2022-02-12 06:38:56 +00:00
|
|
|
logger.debug(f'Headers: {list(request.headers.items())}')
|
2021-07-11 23:30:14 +00:00
|
|
|
|
|
|
|
if domain.split('.')[-1] in NON_TLDS:
|
2021-08-06 17:29:25 +00:00
|
|
|
error(f"{domain} doesn't look like a domain", status=404)
|
2021-07-11 23:30:14 +00:00
|
|
|
|
2023-03-20 21:28:14 +00:00
|
|
|
g.user = User.get_by_id(domain)
|
|
|
|
if not g.user:
|
2023-01-26 04:00:54 +00:00
|
|
|
error(f'No user for {domain}', status=404)
|
|
|
|
|
2022-02-12 06:38:56 +00:00
|
|
|
logger.info(f'Generating WebFinger data for {domain}')
|
2023-03-20 21:28:14 +00:00
|
|
|
actor = g.user.to_as1() or {}
|
2023-02-10 04:00:58 +00:00
|
|
|
logger.info(f'AS1 actor: {actor}')
|
|
|
|
urls = util.dedupe_urls(util.get_list(actor, 'urls') +
|
|
|
|
util.get_list(actor, 'url') +
|
2023-03-20 21:28:14 +00:00
|
|
|
[g.user.homepage])
|
2023-02-10 04:00:58 +00:00
|
|
|
logger.info(f'URLs: {urls}')
|
2021-07-11 23:30:14 +00:00
|
|
|
canonical_url = urls[0]
|
|
|
|
|
|
|
|
# generate webfinger content
|
|
|
|
data = util.trim_nulls({
|
2023-03-20 21:28:14 +00:00
|
|
|
'subject': 'acct:' + g.user.address().lstrip('@'),
|
2021-07-11 23:30:14 +00:00
|
|
|
'aliases': urls,
|
2023-02-10 04:00:58 +00:00
|
|
|
'links':
|
|
|
|
[{
|
2021-07-11 23:30:14 +00:00
|
|
|
'rel': 'http://webfinger.net/rel/profile-page',
|
|
|
|
'type': 'text/html',
|
|
|
|
'href': url,
|
2023-02-10 04:00:58 +00:00
|
|
|
} for url in urls if util.is_web(url)] +
|
|
|
|
|
|
|
|
[{
|
2021-07-11 23:30:14 +00:00
|
|
|
'rel': 'http://webfinger.net/rel/avatar',
|
2023-02-10 04:00:58 +00:00
|
|
|
'href': url,
|
|
|
|
} for url in util.get_urls(actor, 'image')] +
|
|
|
|
|
|
|
|
[{
|
2021-07-11 23:30:14 +00:00
|
|
|
'rel': 'canonical_uri',
|
|
|
|
'type': 'text/html',
|
|
|
|
'href': canonical_url,
|
|
|
|
},
|
|
|
|
|
|
|
|
# ActivityPub
|
|
|
|
{
|
|
|
|
'rel': 'self',
|
2023-01-07 05:01:33 +00:00
|
|
|
'type': as2.CONTENT_TYPE,
|
2021-07-11 23:30:14 +00:00
|
|
|
# WARNING: in python 2 sometimes request.host_url lost port,
|
|
|
|
# http://localhost:8080 would become just http://localhost. no
|
|
|
|
# clue how or why. pay attention here if that happens again.
|
2023-03-20 21:28:14 +00:00
|
|
|
'href': g.user.actor_id(),
|
2021-07-11 23:30:14 +00:00
|
|
|
}, {
|
2023-01-07 17:34:55 +00:00
|
|
|
# AP reads this and sharedInbox from the AS2 actor, not
|
|
|
|
# webfinger, so strictly speaking, it's probably not needed here.
|
2021-07-11 23:30:14 +00:00
|
|
|
'rel': 'inbox',
|
2023-01-07 05:01:33 +00:00
|
|
|
'type': as2.CONTENT_TYPE,
|
2023-01-05 23:03:21 +00:00
|
|
|
'href': common.host_url(f'{domain}/inbox'),
|
2022-11-16 18:09:24 +00:00
|
|
|
}, {
|
|
|
|
# https://www.w3.org/TR/activitypub/#sharedInbox
|
|
|
|
'rel': 'sharedInbox',
|
2023-01-07 05:01:33 +00:00
|
|
|
'type': as2.CONTENT_TYPE,
|
2023-01-05 23:03:21 +00:00
|
|
|
'href': common.host_url('inbox'),
|
2021-07-11 23:30:14 +00:00
|
|
|
},
|
|
|
|
|
2022-12-01 16:15:48 +00:00
|
|
|
# remote follow
|
|
|
|
# https://socialhub.activitypub.rocks/t/what-is-the-current-spec-for-remote-follow/2020/11?u=snarfed
|
|
|
|
# https://github.com/snarfed/bridgy-fed/issues/60#issuecomment-1325589750
|
|
|
|
{
|
|
|
|
'rel': 'http://ostatus.org/schema/1.0/subscribe',
|
2023-01-05 23:03:21 +00:00
|
|
|
'template': common.host_url(f'user/{domain}?url={{uri}}'),
|
2021-07-11 23:30:14 +00:00
|
|
|
}]
|
2021-07-11 20:39:19 +00:00
|
|
|
})
|
2023-01-26 04:41:29 +00:00
|
|
|
|
2022-02-12 06:38:56 +00:00
|
|
|
logger.info(f'Returning WebFinger data: {json_dumps(data, indent=2)}')
|
2021-07-11 23:30:14 +00:00
|
|
|
return data
|
2017-08-19 15:21:05 +00:00
|
|
|
|
2021-07-11 23:30:14 +00:00
|
|
|
|
2022-11-16 06:00:28 +00:00
|
|
|
class Webfinger(Actor):
|
2021-03-12 22:30:24 +00:00
|
|
|
"""Handles Webfinger requests.
|
|
|
|
|
|
|
|
https://webfinger.net/
|
|
|
|
|
|
|
|
Supports both JRD and XRD; defaults to JRD.
|
|
|
|
https://tools.ietf.org/html/rfc7033#section-4
|
|
|
|
"""
|
2021-07-11 23:30:14 +00:00
|
|
|
def template_vars(self):
|
2022-11-25 16:00:14 +00:00
|
|
|
resource = flask_util.get_required_param('resource').strip()
|
2023-01-05 23:03:21 +00:00
|
|
|
resource = resource.removeprefix(common.host_url())
|
2022-11-17 15:56:00 +00:00
|
|
|
|
|
|
|
# handle Bridgy Fed actor URLs, eg https://fed.brid.gy/snarfed.org
|
2023-01-05 23:03:21 +00:00
|
|
|
host = util.domain_from_link(common.host_url())
|
|
|
|
if resource in ('', '/', f'acct:{host}', f'acct:@{host}'):
|
2022-11-25 16:00:14 +00:00
|
|
|
error('Expected other domain, not fed.brid.gy')
|
2022-11-17 15:56:00 +00:00
|
|
|
|
2021-07-11 23:30:14 +00:00
|
|
|
try:
|
|
|
|
user, domain = util.parse_acct_uri(resource)
|
|
|
|
if domain in common.DOMAINS:
|
|
|
|
domain = user
|
|
|
|
except ValueError:
|
|
|
|
domain = urllib.parse.urlparse(resource).netloc or resource
|
|
|
|
|
2023-02-12 02:59:19 +00:00
|
|
|
return super().template_vars(domain=domain)
|
2021-07-11 23:30:14 +00:00
|
|
|
|
|
|
|
|
2021-07-18 04:22:13 +00:00
|
|
|
class HostMeta(flask_util.XrdOrJrd):
|
2021-07-11 23:50:44 +00:00
|
|
|
"""Renders and serves the /.well-known/host-meta file.
|
|
|
|
|
|
|
|
Supports both JRD and XRD; defaults to XRD.
|
|
|
|
https://tools.ietf.org/html/rfc6415#section-3
|
|
|
|
"""
|
2021-07-18 04:22:13 +00:00
|
|
|
DEFAULT_TYPE = flask_util.XrdOrJrd.XRD
|
2021-07-11 23:50:44 +00:00
|
|
|
|
|
|
|
def template_prefix(self):
|
|
|
|
return 'host-meta'
|
|
|
|
|
|
|
|
def template_vars(self):
|
2023-01-05 23:03:21 +00:00
|
|
|
return {'host_uri': common.host_url()}
|
2021-07-11 23:50:44 +00:00
|
|
|
|
|
|
|
|
|
|
|
@app.get('/.well-known/host-meta.xrds')
|
|
|
|
def host_meta_xrds():
|
|
|
|
"""Renders and serves the /.well-known/host-meta.xrds XRDS-Simple file."""
|
2023-01-05 23:03:21 +00:00
|
|
|
return (render_template('host-meta.xrds', host_uri=common.host_url()),
|
2021-07-11 23:50:44 +00:00
|
|
|
{'Content-Type': 'application/xrds+xml'})
|
|
|
|
|
|
|
|
|
2021-07-12 21:19:56 +00:00
|
|
|
app.add_url_rule(f'/acct:<regex("{common.DOMAIN_RE}"):domain>',
|
2022-11-16 06:00:28 +00:00
|
|
|
view_func=Actor.as_view('actor_acct'))
|
2021-07-11 23:30:14 +00:00
|
|
|
app.add_url_rule('/.well-known/webfinger', view_func=Webfinger.as_view('webfinger'))
|
2021-07-11 23:50:44 +00:00
|
|
|
app.add_url_rule('/.well-known/host-meta', view_func=HostMeta.as_view('hostmeta'))
|
|
|
|
app.add_url_rule('/.well-known/host-meta.json', view_func=HostMeta.as_view('hostmeta-json'))
|