kopia lustrzana https://github.com/snarfed/bridgy-fed
switch webfinger to serve entirely out of Users in datastore
no more fetching and converting users' home pages on demand! for #392, helps #378pull/424/head
rodzic
fb324c10da
commit
5d455b7d18
|
@ -1,77 +1,16 @@
|
|||
# coding=utf-8
|
||||
"""Unit tests for webfinger.py.
|
||||
|
||||
to test:
|
||||
* user URL that redirects
|
||||
* error handling
|
||||
"""
|
||||
"""Unit tests for webfinger.py."""
|
||||
import html
|
||||
from unittest import mock
|
||||
import urllib.parse
|
||||
|
||||
from oauth_dropins.webutil import util
|
||||
from oauth_dropins.webutil.testutil import requests_response
|
||||
from oauth_dropins.webutil.util import json_loads
|
||||
import requests
|
||||
from oauth_dropins.webutil.util import json_dumps, json_loads
|
||||
|
||||
import common
|
||||
import models
|
||||
from models import User
|
||||
from . import testutil
|
||||
|
||||
|
||||
class WebfingerTest(testutil.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.html = """
|
||||
<body class="h-card">
|
||||
<a class="u-url" rel="me" href="/about-me">
|
||||
<img class="u-photo" src="/me.jpg" />
|
||||
Mrs. ☕ Foo
|
||||
</a>
|
||||
</body>
|
||||
"""
|
||||
self.key = models.User.get_or_create('foo.com')
|
||||
self.expected_webfinger = {
|
||||
'subject': 'acct:foo.com@foo.com',
|
||||
'aliases': [
|
||||
'https://foo.com/about-me',
|
||||
'https://foo.com/',
|
||||
],
|
||||
'links': [{
|
||||
'rel': 'http://webfinger.net/rel/profile-page',
|
||||
'type': 'text/html',
|
||||
'href': 'https://foo.com/about-me'
|
||||
}, {
|
||||
'rel': 'http://webfinger.net/rel/profile-page',
|
||||
'type': 'text/html',
|
||||
'href': 'https://foo.com/'
|
||||
}, {
|
||||
'rel': 'http://webfinger.net/rel/avatar',
|
||||
'href': 'https://foo.com/me.jpg'
|
||||
}, {
|
||||
'rel': 'canonical_uri',
|
||||
'type': 'text/html',
|
||||
'href': 'https://foo.com/about-me'
|
||||
}, {
|
||||
'rel': 'self',
|
||||
'type': 'application/activity+json',
|
||||
'href': 'http://localhost/foo.com'
|
||||
}, {
|
||||
'rel': 'inbox',
|
||||
'type': 'application/activity+json',
|
||||
'href': 'http://localhost/foo.com/inbox'
|
||||
}, {
|
||||
'rel': 'sharedInbox',
|
||||
'type': 'application/activity+json',
|
||||
'href': 'http://localhost/inbox'
|
||||
}, {
|
||||
'rel': 'http://ostatus.org/schema/1.0/subscribe',
|
||||
'template': 'http://localhost/user/foo.com?url={uri}',
|
||||
}],
|
||||
}
|
||||
|
||||
class HostMetaTest(testutil.TestCase):
|
||||
def test_host_meta_xrd(self):
|
||||
got = self.client.get('/.well-known/host-meta')
|
||||
self.assertEqual(200, got.status_code)
|
||||
|
@ -94,28 +33,73 @@ class WebfingerTest(testutil.TestCase):
|
|||
body = got.get_data(as_text=True)
|
||||
self.assertTrue(body.startswith('{'), body)
|
||||
|
||||
@mock.patch('requests.get')
|
||||
def test_user(self, mock_get):
|
||||
mock_get.return_value = requests_response(self.html, url='https://foo.com/')
|
||||
|
||||
class WebfingerTest(testutil.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.actor_as2 = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'type': 'Person',
|
||||
'url': 'https://foo.com/about-me',
|
||||
'name': 'Mrs. ☕ Foo',
|
||||
'icon': {'type': 'Image', 'url': 'https://foo.com/me.jpg'},
|
||||
}
|
||||
self.user = User.get_or_create('foo.com', has_hcard=True,
|
||||
actor_as2=json_dumps(self.actor_as2))
|
||||
self.user.put()
|
||||
self.expected_webfinger = {
|
||||
'subject': 'acct:foo.com@foo.com',
|
||||
'aliases': [
|
||||
'https://foo.com/about-me',
|
||||
'https://foo.com/',
|
||||
],
|
||||
'links': [{
|
||||
'rel': 'http://webfinger.net/rel/profile-page',
|
||||
'type': 'text/html',
|
||||
'href': 'https://foo.com/about-me',
|
||||
}, {
|
||||
'rel': 'http://webfinger.net/rel/profile-page',
|
||||
'type': 'text/html',
|
||||
'href': 'https://foo.com/',
|
||||
}, {
|
||||
'rel': 'http://webfinger.net/rel/avatar',
|
||||
'href': 'https://foo.com/me.jpg',
|
||||
}, {
|
||||
'rel': 'canonical_uri',
|
||||
'type': 'text/html',
|
||||
'href': 'https://foo.com/about-me',
|
||||
}, {
|
||||
'rel': 'self',
|
||||
'type': 'application/activity+json',
|
||||
'href': 'http://localhost/foo.com',
|
||||
}, {
|
||||
'rel': 'inbox',
|
||||
'type': 'application/activity+json',
|
||||
'href': 'http://localhost/foo.com/inbox'
|
||||
}, {
|
||||
'rel': 'sharedInbox',
|
||||
'type': 'application/activity+json',
|
||||
'href': 'http://localhost/inbox',
|
||||
}, {
|
||||
'rel': 'http://ostatus.org/schema/1.0/subscribe',
|
||||
'template': 'http://localhost/user/foo.com?url={uri}',
|
||||
}],
|
||||
}
|
||||
|
||||
def test_user(self):
|
||||
got = self.client.get('/acct:foo.com', headers={'Accept': 'application/json'})
|
||||
self.assertEqual(200, got.status_code)
|
||||
self.assertEqual('application/jrd+json', got.headers['Content-Type'])
|
||||
self.assert_req(mock_get, 'https://foo.com/')
|
||||
|
||||
self.assertEqual(self.expected_webfinger, got.json)
|
||||
|
||||
@mock.patch('requests.get')
|
||||
def test_user_no_hcard(self, mock_get):
|
||||
mock_get.return_value = requests_response("""
|
||||
<body>
|
||||
<div class="h-entry">
|
||||
<p class="e-content">foo bar</p>
|
||||
</div>
|
||||
</body>
|
||||
""", url='https://foo.com/')
|
||||
def test_user_no_hcard(self):
|
||||
self.user.has_hcard = False
|
||||
self.user.actor_as2 = None
|
||||
self.user.put()
|
||||
|
||||
got = self.client.get('/acct:foo.com')
|
||||
self.assert_req(mock_get, 'https://foo.com/')
|
||||
self.assertEqual(200, got.status_code)
|
||||
self.assert_equals({
|
||||
'subject': 'acct:foo.com@foo.com',
|
||||
|
@ -156,10 +140,7 @@ class WebfingerTest(testutil.TestCase):
|
|||
got = self.client.get('/acct:nope.com', headers={'Accept': 'application/json'})
|
||||
self.assertEqual(404, got.status_code)
|
||||
|
||||
@mock.patch('requests.get')
|
||||
def test_webfinger(self, mock_get):
|
||||
mock_get.return_value = requests_response(self.html, url='https://foo.com/')
|
||||
|
||||
def test_webfinger(self):
|
||||
for resource in ('foo.com@foo.com', 'acct:foo.com@foo.com', 'xyz@foo.com',
|
||||
'foo.com', 'http://foo.com/', 'https://foo.com/',
|
||||
'http://localhost/foo.com'):
|
||||
|
@ -171,18 +152,17 @@ class WebfingerTest(testutil.TestCase):
|
|||
self.assertEqual('application/jrd+json', got.headers['Content-Type'])
|
||||
self.assertEqual(self.expected_webfinger, got.json)
|
||||
|
||||
@mock.patch('requests.get')
|
||||
def test_webfinger_custom_username(self, mock_get):
|
||||
self.html = """
|
||||
<body class="h-card">
|
||||
<a class="u-url" rel="me" href="/about-me">
|
||||
<img class="u-photo" src="/me.jpg" />
|
||||
Mrs. ☕ Foo
|
||||
</a>
|
||||
<a class="u-url" href="acct:notthisuser@boop.org"></a>
|
||||
<a class="u-url" href="acct:customuser@foo.com"></a>
|
||||
</body>
|
||||
"""
|
||||
def test_webfinger_custom_username(self):
|
||||
self.user.actor_as2 = json_dumps({
|
||||
**self.actor_as2,
|
||||
'url': [
|
||||
'https://foo.com/about-me',
|
||||
'acct:notthisuser@boop.org',
|
||||
'acct:customuser@foo.com',
|
||||
],
|
||||
})
|
||||
self.user.put()
|
||||
|
||||
self.expected_webfinger.update({
|
||||
'subject': 'acct:customuser@foo.com',
|
||||
'aliases': [
|
||||
|
@ -192,7 +172,6 @@ class WebfingerTest(testutil.TestCase):
|
|||
'https://foo.com/',
|
||||
],
|
||||
})
|
||||
mock_get.return_value = requests_response(self.html, url='https://foo.com/')
|
||||
|
||||
for resource in (
|
||||
'customuser@foo.com',
|
||||
|
|
62
webfinger.py
62
webfinger.py
|
@ -9,11 +9,10 @@ import re
|
|||
import urllib.parse
|
||||
|
||||
from flask import render_template, request
|
||||
from granary import as2, atom, microformats2
|
||||
import mf2util
|
||||
from granary import as2
|
||||
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 oauth_dropins.webutil.util import json_dumps, json_loads
|
||||
|
||||
from app import app, cache
|
||||
import common
|
||||
|
@ -25,11 +24,8 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class Actor(flask_util.XrdOrJrd):
|
||||
"""Fetches a site's home page, converts its mf2 to WebFinger, and serves.
|
||||
|
||||
TODO: unify with common.actor()
|
||||
"""
|
||||
@flask_util.cached(cache, common.CACHE_TIME, headers=['Accept'], http_5xx=True)
|
||||
"""Fetches a site's home page, converts its mf2 to WebFinger, and serves."""
|
||||
@flask_util.cached(cache, common.CACHE_TIME, headers=['Accept'])
|
||||
def dispatch_request(self, *args, **kwargs):
|
||||
return super().dispatch_request(*args, **kwargs)
|
||||
|
||||
|
@ -46,52 +42,32 @@ class Actor(flask_util.XrdOrJrd):
|
|||
if not user:
|
||||
error(f'No user for {domain}', status=404)
|
||||
|
||||
# find representative h-card. try url, then url's home page, then domain
|
||||
urls = [f'https://{domain}/']
|
||||
if url:
|
||||
urls = [url, urllib.parse.urljoin(url, '/')] + urls
|
||||
|
||||
resp = None
|
||||
for candidate in urls:
|
||||
try:
|
||||
resp = util.requests_get(candidate)
|
||||
except ValueError:
|
||||
logger.warning('', exc_info=True)
|
||||
continue
|
||||
parsed = util.parse_html(resp)
|
||||
mf2 = util.parse_mf2(parsed, url=resp.url)
|
||||
# logger.debug(f'Parsed mf2 for {resp.url}: {json_dumps(mf2, indent=2)}')
|
||||
hcard = mf2util.representative_hcard(mf2, resp.url)
|
||||
if hcard:
|
||||
logger.info(f'Representative h-card: {json_dumps(hcard, indent=2)}')
|
||||
user.actor_as2 = json_dumps(common.postprocess_as2(
|
||||
as2.from_as1(microformats2.json_to_object(hcard)), user=user))
|
||||
user.put()
|
||||
break
|
||||
else:
|
||||
logger.info(f"didn't find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) in any of {urls}")
|
||||
hcard = {'properties': {
|
||||
'url': [f'https://{domain}/'],
|
||||
}}
|
||||
|
||||
logger.info(f'Generating WebFinger data for {domain}')
|
||||
props = hcard.get('properties', {})
|
||||
urls = util.dedupe_urls(props.get('url', []) +
|
||||
([resp.url] if resp else []))
|
||||
actor = as2.to_as1(json_loads(user.actor_as2) if user.actor_as2 else {})
|
||||
logger.info(f'AS1 actor: {actor}')
|
||||
urls = util.dedupe_urls(util.get_list(actor, 'urls') +
|
||||
util.get_list(actor, 'url') +
|
||||
[f'https://{domain}'])
|
||||
logger.info(f'URLs: {urls}')
|
||||
canonical_url = urls[0]
|
||||
|
||||
# generate webfinger content
|
||||
data = util.trim_nulls({
|
||||
'subject': 'acct:' + user.address().lstrip('@'),
|
||||
'aliases': urls,
|
||||
'links': sum(([{
|
||||
'links':
|
||||
[{
|
||||
'rel': 'http://webfinger.net/rel/profile-page',
|
||||
'type': 'text/html',
|
||||
'href': url,
|
||||
}] for url in urls if url.startswith("http")), []) + [{
|
||||
} for url in urls if util.is_web(url)] +
|
||||
|
||||
[{
|
||||
'rel': 'http://webfinger.net/rel/avatar',
|
||||
'href': microformats2.get_text(url),
|
||||
} for url in props.get('photo', [])] + [{
|
||||
'href': url,
|
||||
} for url in util.get_urls(actor, 'image')] +
|
||||
|
||||
[{
|
||||
'rel': 'canonical_uri',
|
||||
'type': 'text/html',
|
||||
'href': canonical_url,
|
||||
|
|
Ładowanie…
Reference in New Issue