kopia lustrzana https://github.com/snarfed/bridgy-fed
179 wiersze
5.9 KiB
Python
179 wiersze
5.9 KiB
Python
"""Handles requests for WebFinger endpoints.
|
|
|
|
https://webfinger.net/
|
|
https://tools.ietf.org/html/rfc7033
|
|
|
|
Largely based on webfinger-unofficial/user.py.
|
|
|
|
TODO: test:
|
|
* /.well-known/webfinger
|
|
* acct: URI handling
|
|
* user URL that redirects
|
|
"""
|
|
from __future__ import unicode_literals
|
|
import datetime
|
|
import json
|
|
import logging
|
|
import urllib
|
|
import urlparse
|
|
|
|
import appengine_config
|
|
|
|
import mf2util
|
|
from oauth_dropins.webutil import handlers, util
|
|
import webapp2
|
|
|
|
import common
|
|
import models
|
|
|
|
CACHE_TIME = datetime.timedelta(seconds=15)
|
|
NON_TLDS = frozenset(('html', 'json', 'php', 'xml'))
|
|
|
|
|
|
class UserHandler(handlers.XrdOrJrdHandler):
|
|
"""Fetches a site's home page, converts its mf2 to WebFinger, and serves."""
|
|
JRD_TEMPLATE = False
|
|
|
|
@handlers.memcache_response(CACHE_TIME)
|
|
def get(self, *args, **kwargs):
|
|
return super(UserHandler, self).get(*args, **kwargs)
|
|
|
|
def template_prefix(self):
|
|
return 'templates/webfinger_user'
|
|
|
|
def template_vars(self, domain, url=None):
|
|
assert domain
|
|
|
|
if domain.split('.')[-1] in NON_TLDS:
|
|
common.error(self, "%s doesn't look like a domain" % domain, status=404)
|
|
|
|
# find representative h-card. try url, then url's home page, then domain
|
|
urls = ['http://%s/' % domain]
|
|
if url:
|
|
urls = [url, urlparse.urljoin(url, '/')] + urls
|
|
|
|
for candidate in urls:
|
|
resp = common.requests_get(candidate)
|
|
parsed = util.parse_html(resp)
|
|
mf2 = util.parse_mf2(parsed, url=resp.url)
|
|
# logging.debug('Parsed mf2 for %s: %s', resp.url, json.dumps(mf2, indent=2))
|
|
hcard = mf2util.representative_hcard(mf2, resp.url)
|
|
if hcard:
|
|
logging.info('Representative h-card: %s', json.dumps(hcard, indent=2))
|
|
break
|
|
else:
|
|
common.error(self, """\
|
|
Couldn't find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on %s""" % resp.url)
|
|
|
|
logging.info('Generating WebFinger data for %s', domain)
|
|
key = models.MagicKey.get_or_create(domain)
|
|
props = hcard.get('properties', {})
|
|
urls = util.dedupe_urls(props.get('url', []) + [resp.url])
|
|
canonical_url = urls[0]
|
|
|
|
acct = '%s@%s' % (domain, domain)
|
|
for url in urls:
|
|
if url.startswith('acct:'):
|
|
urluser, urldomain = util.parse_acct_uri(url)
|
|
if urldomain == domain:
|
|
acct = '%s@%s' % (urluser, domain)
|
|
logging.info('Found custom username: acct:%s', acct)
|
|
break
|
|
|
|
# discover atom feed, if any
|
|
atom = parsed.find('link', rel='alternate', type=common.CONTENT_TYPE_ATOM)
|
|
if atom and atom['href']:
|
|
atom = urlparse.urljoin(resp.url, atom['href'])
|
|
else:
|
|
atom = 'https://granary.io/url?' + urllib.urlencode({
|
|
'input': 'html',
|
|
'output': 'atom',
|
|
'url': resp.url,
|
|
'hub': resp.url,
|
|
})
|
|
|
|
# discover PuSH, if any
|
|
for link in resp.headers.get('Link', '').split(','):
|
|
match = common.LINK_HEADER_RE.match(link)
|
|
if match and match.group(2) == 'hub':
|
|
hub = match.group(1)
|
|
else:
|
|
hub = 'https://bridgy-fed.superfeedr.com/'
|
|
|
|
|
|
# generate webfinger content
|
|
data = util.trim_nulls({
|
|
'subject': 'acct:' + acct,
|
|
'aliases': urls,
|
|
'magic_keys': [{'value': key.href()}],
|
|
'links': sum(([{
|
|
'rel': 'http://webfinger.net/rel/profile-page',
|
|
'type': 'text/html',
|
|
'href': url,
|
|
}] for url in urls if url.startswith("http")), []) + [{
|
|
'rel': 'http://webfinger.net/rel/avatar',
|
|
'href': url,
|
|
} for url in props.get('photo', [])] + [{
|
|
'rel': 'canonical_uri',
|
|
'type': 'text/html',
|
|
'href': canonical_url,
|
|
},
|
|
|
|
# ActivityPub
|
|
{
|
|
'rel': 'self',
|
|
'type': 'application/activity+json',
|
|
# use HOST_URL instead of e.g. request.host_url because it
|
|
# sometimes lost port, e.g. http://localhost:8080 would become
|
|
# just http://localhost. no clue how or why.
|
|
'href': '%s/%s' % (appengine_config.HOST_URL, domain),
|
|
}, {
|
|
'rel': 'inbox',
|
|
'type': 'application/activity+json',
|
|
'href': '%s/%s/inbox' % (appengine_config.HOST_URL, domain),
|
|
},
|
|
|
|
# OStatus
|
|
{
|
|
'rel': 'http://schemas.google.com/g/2010#updates-from',
|
|
'type': common.CONTENT_TYPE_ATOM,
|
|
'href': atom,
|
|
}, {
|
|
'rel': 'hub',
|
|
'href': hub,
|
|
}, {
|
|
'rel': 'magic-public-key',
|
|
'href': key.href(),
|
|
}, {
|
|
'rel': 'salmon',
|
|
'href': '%s/%s/salmon' % (appengine_config.HOST_URL, domain),
|
|
}]
|
|
})
|
|
logging.info('Returning WebFinger data: %s', json.dumps(data, indent=2))
|
|
return data
|
|
|
|
|
|
class WebfingerHandler(UserHandler):
|
|
|
|
def is_jrd(self):
|
|
return True
|
|
|
|
def template_vars(self):
|
|
resource = util.get_required_param(self, 'resource')
|
|
try:
|
|
_, domain = util.parse_acct_uri(resource)
|
|
except ValueError:
|
|
domain = urlparse.urlparse(resource).netloc or resource
|
|
|
|
url = None
|
|
if resource.startswith('http://') or resource.startswith('https://'):
|
|
url = resource
|
|
|
|
return super(WebfingerHandler, self).template_vars(domain, url=url)
|
|
|
|
|
|
app = webapp2.WSGIApplication([
|
|
(r'/%s/?' % common.DOMAIN_RE, UserHandler),
|
|
('/.well-known/webfinger', WebfingerHandler),
|
|
] + handlers.HOST_META_ROUTES, debug=appengine_config.DEBUG)
|