diff --git a/tests/test_activitypub.py b/tests/test_activitypub.py index 064e94a..f2c1fa4 100644 --- a/tests/test_activitypub.py +++ b/tests/test_activitypub.py @@ -323,7 +323,7 @@ class ActivityPubTest(TestCase): 'type': 'Person', 'id': 'http://bf/fake/user.com/ap', 'preferredUsername': 'user.com', - 'url': 'http://localhost/r/fake://user.com', + 'url': 'http://localhost/r/user.com', 'summary': '', 'inbox': 'http://bf/fake/user.com/ap/inbox', 'outbox': 'http://bf/fake/user.com/ap/outbox', @@ -1456,16 +1456,16 @@ class ActivityPubUtilsTest(TestCase): 'actor': { 'id': 'baj', 'preferredUsername': 'site', - 'url': 'http://localhost/r/fake://site', + 'url': 'http://localhost/r/site', }, 'attributedTo': [{ 'id': 'bar', 'preferredUsername': 'site', - 'url': 'http://localhost/r/fake://site', + 'url': 'http://localhost/r/site', }, { 'id': 'baz', 'preferredUsername': 'site', - 'url': 'http://localhost/r/fake://site', + 'url': 'http://localhost/r/site', }], 'to': [as2.PUBLIC_AUDIENCE], }, activitypub.postprocess_as2({ diff --git a/tests/test_common.py b/tests/test_common.py index 45f8abe..b4d25be 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -14,6 +14,7 @@ import common from flask_app import app from models import Object, User import protocol +from web import Web class CommonTest(TestCase): @@ -47,8 +48,10 @@ class CommonTest(TestCase): common.pretty_link('http://foo')) + # current user's homepage gets converted to BF user page + g.user = Web(id='user.com') self.assertEqual( - ' user.com', + ' user.com', common.pretty_link('https://user.com/')) def test_redirect_wrap_empty(self): diff --git a/tests/test_models.py b/tests/test_models.py index 5d3dc36..d48c45a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -219,10 +219,10 @@ class FollowerTest(TestCase): def test_from_to_same_type_fails(self): with self.assertRaises(AssertionError): - Follower(from_=Web.key_for('foo'), to=Web.key_for('bar')).put() + Follower(from_=Web.key_for('foo.com'), to=Web.key_for('bar.com')).put() with self.assertRaises(AssertionError): - Follower.get_or_create(from_=Web(id='foo'), to=Web(id='bar')) + Follower.get_or_create(from_=Web(id='foo.com'), to=Web(id='bar.com')) def test_get_or_create(self): follower = Follower.get_or_create(from_=g.user, to=self.other_user) diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 468610f..c02a189 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -115,7 +115,7 @@ class ProtocolTest(TestCase): (None, None), ('', None), ('foo://bar', None), - ('fake://foo', Fake), + ('fake:foo', Fake), # TODO # ('at://foo', ATProto), ('https://ap.brid.gy/foo/bar', ActivityPub), diff --git a/tests/test_web.py b/tests/test_web.py index 42dcf77..bc8f583 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -1707,8 +1707,8 @@ class WebProtocolTest(TestCase): for id in 'user.com', 'http://user.com', 'https://user.com/': self.assertEqual(Web(id='user.com').key, Web.key_for(id)) - for bad in None, '', 'foo', 'https://foo/', 'foo bar': - with self.assertRaises(AssertionError): + for bad in None, '', 'foo', 'https://foo/', 'foo bar', 'user.json': + with self.subTest(bad=bad), self.assertRaises(ValueError): Web.key_for(bad) def test_owns_id(self, *_): diff --git a/tests/test_webfinger.py b/tests/test_webfinger.py index b416c1d..03abbed 100644 --- a/tests/test_webfinger.py +++ b/tests/test_webfinger.py @@ -82,32 +82,32 @@ WEBFINGER_NO_HCARD = { }], } WEBFINGER_FAKE = { - 'subject': 'acct:user@fake', - 'aliases': ['fake://user/'], + 'subject': 'acct:fake:user@fake', + 'aliases': ['fake:user'], 'links': [{ 'rel': 'canonical_uri', 'type': 'text/html', - 'href': 'fake://user/', + 'href': 'fake:user', }, { 'rel': 'self', 'type': 'application/activity+json', - 'href': 'http://bf/fake/user/ap', + 'href': 'http://bf/fake/fake:user/ap', }, { 'rel': 'inbox', 'type': 'application/activity+json', - 'href': 'http://bf/fake/user/ap/inbox', + 'href': 'http://bf/fake/fake:user/ap/inbox', }, { 'rel': 'sharedInbox', 'type': 'application/activity+json', 'href': 'http://localhost/ap/sharedInbox', }, { 'rel': 'http://ostatus.org/schema/1.0/subscribe', - 'template': 'http://localhost/fa/user?url={uri}', + 'template': 'http://localhost/fa/fake:user?url={uri}', }], } WEBFINGER_FAKE_FED_BRID_GY = copy.deepcopy(WEBFINGER_FAKE) WEBFINGER_FAKE_FED_BRID_GY['links'][3]['href'] = 'https://fed.brid.gy/ap/sharedInbox' -WEBFINGER_FAKE_FED_BRID_GY['links'][4]['template'] = 'https://fed.brid.gy/fa/user?url={uri}' +WEBFINGER_FAKE_FED_BRID_GY['links'][4]['template'] = 'https://fed.brid.gy/fa/fake:user?url={uri}' class HostMetaTest(TestCase): @@ -162,33 +162,37 @@ class WebfingerTest(TestCase): self.assert_equals(WEBFINGER, got.json) def test_user_infer_protocol_from_resource_subdomain(self): - got = self.client.get('/.well-known/webfinger?resource=acct:user@fake.brid.gy', - headers={'Accept': 'application/json'}) + got = self.client.get( + '/.well-known/webfinger?resource=acct:fake:user@fake.brid.gy', + headers={'Accept': 'application/json'}) self.assertEqual(200, got.status_code) self.assertEqual('application/jrd+json', got.headers['Content-Type']) self.assert_equals(WEBFINGER_FAKE, got.json) def test_user_infer_protocol_from_request_subdomain(self): - self.make_user('user', cls=Fake) - got = self.client.get('/.well-known/webfinger?resource=acct:user@user', - base_url='https://fake.brid.gy/', - headers={'Accept': 'application/json'}) + self.make_user('fake:user', cls=Fake) + got = self.client.get( + '/.well-known/webfinger?resource=acct:user@fake:user', + base_url='https://fake.brid.gy/', + headers={'Accept': 'application/json'}) self.assertEqual(200, got.status_code) self.assertEqual('application/jrd+json', got.headers['Content-Type']) self.assert_equals(WEBFINGER_FAKE_FED_BRID_GY, got.json) def test_user_infer_protocol_resource_overrides_request(self): - got = self.client.get('/.well-known/webfinger?resource=acct:user@fake.brid.gy', - base_url='https://ap.brid.gy/', - headers={'Accept': 'application/json'}) + got = self.client.get( + '/.well-known/webfinger?resource=acct:fake:user@fake.brid.gy', + base_url='https://ap.brid.gy/', + headers={'Accept': 'application/json'}) self.assertEqual(200, got.status_code) self.assertEqual('application/jrd+json', got.headers['Content-Type']) self.assert_equals(WEBFINGER_FAKE_FED_BRID_GY, got.json) def test_urlencoded(self): """https://github.com/snarfed/bridgy-fed/issues/535""" - got = self.client.get('/.well-known/webfinger?resource=acct%3Auser.com%40user.com', - headers={'Accept': 'application/json'}) + got = self.client.get( + '/.well-known/webfinger?resource=acct%3Auser.com%40user.com', + headers={'Accept': 'application/json'}) self.assertEqual(200, got.status_code, got.get_data(as_text=True)) self.assertEqual('application/jrd+json', got.headers['Content-Type']) self.assert_equals(WEBFINGER, got.json) @@ -216,7 +220,6 @@ class WebfingerTest(TestCase): # Mastodon requires this as of 3.3.0 # https://github.com/snarfed/bridgy-fed/issues/73 'acct:user.com@fed.brid.gy', - 'acct:user.com@bridgy-federated.appspot.com', 'acct:user.com@localhost', ): with self.subTest(resource=resource): @@ -237,14 +240,31 @@ class WebfingerTest(TestCase): }, got.json) def test_missing_user(self): - for bad in 'nope.com', 'nope.com@nope.com': - got = self.client.get(f'/.well-known/webfinger?resource=acct:{bad}') - self.assertEqual(404, got.status_code) + got = self.client.get(f'/.well-known/webfinger?resource=acct:nope.com@nope.com') + self.assertEqual(404, got.status_code) + + got = self.client.get(f'/.well-known/webfinger?resource=acct:nope.com') + self.assertEqual(400, got.status_code) + + def test_indirect_user_not_on_bridgy_fed_subdomain(self): + self.user.direct = False + self.user.put() + got = self.client.get(f'/.well-known/webfinger?resource=acct:user.com@user.com') + self.assertEqual(404, got.status_code) + + def test_bad_id(self): + got = self.client.get(f'/.well-known/webfinger?resource=acct:nope@fa.brid.gy') + self.assertEqual(400, got.status_code, got.get_data(as_text=True)) + + got = self.client.get(f'/.well-known/webfinger?resource=acct:nope@nope', + base_url='https://fa.brid.gy/') + self.assertEqual(400, got.status_code, got.get_data(as_text=True)) def test_bad_tld(self): self.make_user('user.json') - got = self.client.get(f'/.well-known/webfinger?resource=acct:user.json@user.json') - self.assertIn("like a domain but", html.unescape(got.get_data(as_text=True))) + got = self.client.get(f'/.well-known/webfinger?resource=acct:user.json@user.json', + base_url='https://web.brid.gy/') + self.assertEqual(400, got.status_code, got.get_data(as_text=True)) @patch('requests.get') def test_fetch_create_user(self, mock_get): diff --git a/tests/testutil.py b/tests/testutil.py index 969f2c9..0463625 100644 --- a/tests/testutil.py +++ b/tests/testutil.py @@ -48,7 +48,7 @@ class Fake(User, protocol.Protocol): fetched = [] def web_url(self): - return f'fake://{self.key.id()}' + return self.key.id() def ap_address(self): return f'@{self.key.id()}@fake' @@ -58,7 +58,10 @@ class Fake(User, protocol.Protocol): @classmethod def owns_id(cls, id): - return id.startswith('fake://') + if id.startswith('nope'): + return False + + return id.startswith('fake:') or id in cls.objects @classmethod def send(cls, obj, url, log_data=True): @@ -89,7 +92,7 @@ class Fake(User, protocol.Protocol): # expensive to generate requests.post(f'http://{ndb_client.host}/reset') with ndb_client.context(): - global_user = Fake.get_or_create('user') + global_user = Fake.get_or_create('fake:user') # import other modules that register Flask handlers *after* Fake is defined diff --git a/web.py b/web.py index c6a980a..434fb11 100644 --- a/web.py +++ b/web.py @@ -39,6 +39,7 @@ CHAR_AFTER_SPACE = chr(ord(' ') + 1) WWW_DOMAINS = frozenset(( 'www.jvt.me', )) +NON_TLDS = frozenset(('html', 'json', 'php', 'xml')) class Web(User, Protocol): @@ -211,8 +212,12 @@ class Web(User, Protocol): Args: id: str + + Raises: + ValueError """ - assert id + if not id: + raise ValueError() if util.is_web(id): parsed = urlparse(id) @@ -220,13 +225,16 @@ class Web(User, Protocol): id = parsed.netloc if re.match(common.DOMAIN_RE, id): + tld = id.split('.')[-1] + if tld in NON_TLDS: + raise ValueError(f"{id} looks like a domain but {tld} isn't a TLD") return cls(id=id).key - assert False, f'{id} is not a domain or usable home page URL' + raise ValueError(f'{id} is not a domain or usable home page URL') @classmethod def owns_id(cls, id): - """Returns None if id is an http(s) URL, False otherwise. + """Returns None if id is a domain or http(s) URL, False otherwise. All web pages are http(s) URLs, but not all http(s) URLs are web pages. """ @@ -238,8 +246,8 @@ class Web(User, Protocol): if key: user = key.get() return True if user and user.has_redirects else None - except AssertionError: - pass + except ValueError as e: + logger.info(e) return None if util.is_web(id) else False diff --git a/webfinger.py b/webfinger.py index 41970fe..9c26525 100644 --- a/webfinger.py +++ b/webfinger.py @@ -20,8 +20,6 @@ from models import User from protocol import Protocol from web import Web -NON_TLDS = frozenset(('html', 'json', 'php', 'xml')) - SUBSCRIBE_LINK_REL = 'http://ostatus.org/schema/1.0/subscribe' logger = logging.getLogger(__name__) @@ -55,8 +53,8 @@ class Webfinger(flask_util.XrdOrJrd): cls = None try: user, id = util.parse_acct_uri(resource) - if id in common.DOMAINS: - cls = Protocol.for_domain(id) + cls = Protocol.for_domain(id, fed=Web) + if cls: id = user allow_indirect=True except ValueError: @@ -66,12 +64,8 @@ class Webfinger(flask_util.XrdOrJrd): cls = Protocol.for_request(fed=Web) logger.info(f'Protocol {cls.__name__}, user id {id}') - - # if id is a domain, validate - if re.match(common.DOMAIN_RE, id): - tld = id.split('.')[-1] - if tld in NON_TLDS: - error(f"{id} looks like a domain but {tld} isn't a TLD", status=404) + if cls.owns_id(id) is False: + error(f'{id} is not a valid {cls.__name__} id') # only allow indirect users if this id is "on" a brid.gy subdomain, # eg user.com@bsky.brid.gy but not user.com@user.com @@ -79,6 +73,8 @@ class Webfinger(flask_util.XrdOrJrd): g.user = cls.get_or_create(id) else: g.user = cls.get_by_id(id) + if g.user and not g.user.direct: + error(f"{g.user.key} hasn't signed up yet", status=404) if not g.user: error(f'No {cls.LABEL} user found for {id}', status=404)