diff --git a/tests/test_web.py b/tests/test_web.py index e0876b2..76b9c19 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -1916,6 +1916,8 @@ http://this/404s self.assertFalse(self.user.is_web_url('https://other.com/')) def test_handle_as(self, *_): + self.user.ap_subdomain = 'web' + self.assertEqual('@user.com@user.com', self.user.handle_as(ActivityPub)) self.user.obj = Object(id='a', as2={'type': 'Person'}) @@ -1937,6 +1939,9 @@ http://this/404s self.assertEqual('fake:handle:user.com', self.user.handle_as(Fake)) self.assertEqual('user.com.web.brid.gy', self.user.handle_as('atproto')) + self.user.ap_subdomain = 'fed' + self.assertEqual('@user.com@fed.brid.gy', self.user.handle_as(ActivityPub)) + def test_ap_actor(self, *_): self.assertEqual('http://localhost/user.com', self.user.ap_actor()) diff --git a/tests/test_webfinger.py b/tests/test_webfinger.py index 7d05b9d..0c69d5d 100644 --- a/tests/test_webfinger.py +++ b/tests/test_webfinger.py @@ -49,6 +49,7 @@ WEBFINGER = { 'href': 'https://web.brid.gy/ap/sharedInbox', }, { 'rel': 'http://ostatus.org/schema/1.0/subscribe', + # TODO: genericize 'template': 'https://fed.brid.gy/web/user.com?url={uri}', }], } @@ -66,11 +67,11 @@ WEBFINGER_NO_HCARD = { }, { 'rel': 'self', 'type': 'application/activity+json', - 'href': 'http://localhost/user.com', + 'href': 'https://web.brid.gy/user.com', }, { 'rel': 'inbox', 'type': 'application/activity+json', - 'href': 'http://localhost/user.com/inbox', + 'href': 'https://web.brid.gy/user.com/inbox', }, { 'rel': 'sharedInbox', 'type': 'application/activity+json', @@ -162,6 +163,20 @@ class WebfingerTest(TestCase): self.assertEqual('application/jrd+json', got.headers['Content-Type']) self.assert_equals(WEBFINGER, got.json) + def test_webfinger_web_subdomain_redirects(self): + path = '/.well-known/webfinger?resource=user.com@user.com' + + got = self.client.get(path, base_url='https://fed.brid.gy/') + self.assertEqual(302, got.status_code) + self.assertEqual(f'https://web.brid.gy{path}', got.headers['Location']) + + self.user.ap_subdomain = 'fed' + self.user.put() + + got = self.client.get(path, base_url='https://web.brid.gy/') + self.assertEqual(302, got.status_code) + self.assertEqual(f'https://fed.brid.gy{path}', got.headers['Location']) + def test_user_infer_protocol_from_resource_subdomain(self): got = self.client.get( '/.well-known/webfinger?resource=acct:fake:handle:user@fake.brid.gy', @@ -298,8 +313,10 @@ class WebfingerTest(TestCase): expected = copy.deepcopy(WEBFINGER_NO_HCARD) expected['subject'] = 'acct:user.com@web.brid.gy' - got = self.client.get('/.well-known/webfinger?resource=acct:user.com@fed.brid.gy', - headers={'Accept': 'application/json'}) + got = self.client.get( + '/.well-known/webfinger?resource=acct:user.com@web.brid.gy', + headers={'Accept': 'application/json'}, + base_url='https://web.brid.gy/') self.assertEqual(200, got.status_code) self.assertEqual(expected, got.json) diff --git a/web.py b/web.py index 783f9d5..50155a7 100644 --- a/web.py +++ b/web.py @@ -76,6 +76,13 @@ class Web(User, Protocol): redirects_error = ndb.TextProperty() has_hcard = ndb.BooleanProperty() + # Originally, BF served Web users' AP actor ids on fed.brid.gy, eg + # https://fed.brid.gy/snarfed.org . When we started adding new protocols, we + # switched to per-protocol subdomains, eg https://web.brid.gy/snarfed.org . + # However, we need to preserve the old users' actor ids as is. So, this + # property tracks which subdomain a given Web user's AP actor uses. + ap_subdomain = ndb.StringProperty(choices=['fed', 'web'], default='web') + @classmethod def _get_kind(cls): return 'MagicKey' @@ -114,7 +121,7 @@ class Web(User, Protocol): """Special case ActivityPub to use custom username.""" if to_proto in ('activitypub', 'ap', PROTOCOLS['ap']): return (f'@{self.username()}@{self.key.id()}' if self.has_redirects - else f'@{self.key.id()}@{self.ABBREV}{SUPERDOMAIN}') + else f'@{self.key.id()}@{self.ap_subdomain}{SUPERDOMAIN}') return super().handle_as(to_proto) diff --git a/webfinger.py b/webfinger.py index 8eb8aec..f83b15b 100644 --- a/webfinger.py +++ b/webfinger.py @@ -4,16 +4,17 @@ * https://tools.ietf.org/html/rfc7033 """ import logging -import urllib.parse +from urllib.parse import urljoin, urlparse -from flask import g, render_template, request +from flask import g, redirect, render_template, request from granary import as2 from oauth_dropins.webutil import flask_util, util -from oauth_dropins.webutil.flask_util import error, flash +from oauth_dropins.webutil.flask_util import error, flash, Found from oauth_dropins.webutil.util import json_dumps, json_loads import activitypub import common +from common import LOCAL_DOMAINS, SUPERDOMAIN from flask_app import app, cache from protocol import Protocol @@ -55,7 +56,7 @@ class Webfinger(flask_util.XrdOrJrd): id = user allow_indirect = True except ValueError: - id = urllib.parse.urlparse(resource).netloc or resource + id = urlparse(resource).netloc or resource if not cls: cls = Protocol.for_request(fed='web') @@ -89,6 +90,15 @@ class Webfinger(flask_util.XrdOrJrd): if not user: error(f'No {cls.LABEL} user found for {id}', status=404) + # backward compatibility for initial Web users whose AP actor ids are on + # fed.brid.gy, not web.brid.gy + subdomain = request.host.split('.')[0] + if (user.LABEL == 'web' + and subdomain not in (LOCAL_DOMAINS + (user.ap_subdomain,))): + url = urljoin(f'https://{user.ap_subdomain}{common.SUPERDOMAIN}/', + request.full_path) + raise Found(location=url) + actor = user.obj.as1 if user.obj and user.obj.as1 else {} logger.info(f'Generating WebFinger data for {user.key}') logger.info(f'AS1 actor: {actor}')