From 9489204d645f8deaeef00b33ef14fb8a87c5da8a Mon Sep 17 00:00:00 2001 From: Ryan Barrett Date: Tue, 27 Feb 2024 11:38:00 -0800 Subject: [PATCH] AP: add profile to Content-Type: application/ld+json for #895 --- activitypub.py | 20 ++++++++++---------- redirect.py | 4 +++- tests/test_activitypub.py | 6 ++---- tests/test_redirect.py | 18 +++++++++--------- tests/test_web.py | 3 ++- tests/test_webfinger.py | 19 ++++++++++--------- tests/testutil.py | 2 +- webfinger.py | 6 +++--- 8 files changed, 40 insertions(+), 38 deletions(-) diff --git a/activitypub.py b/activitypub.py index 117f20e..c635a34 100644 --- a/activitypub.py +++ b/activitypub.py @@ -41,7 +41,7 @@ import webfinger logger = logging.getLogger(__name__) CONNEG_HEADERS_AS2_HTML = { - 'Accept': f'{as2.CONNEG_HEADERS["Accept"]}, {CONTENT_TYPE_HTML}; q=0.7' + 'Accept': f'{as2.CONNEG_HEADERS["Accept"]}, {CONTENT_TYPE_HTML}; q=0.5' } HTTP_SIG_HEADERS = ('Date', 'Host', 'Digest', '(request-target)') @@ -67,7 +67,7 @@ class ActivityPub(User, Protocol): ABBREV = 'ap' PHRASE = 'the fediverse' LOGO_HTML = '' - CONTENT_TYPE = as2.CONTENT_TYPE + CONTENT_TYPE = as2.CONTENT_TYPE_LD_PROFILE HAS_FOLLOW_ACCEPTS = True def _pre_put_hook(self): @@ -516,7 +516,7 @@ def signed_request(fn, url, data=None, headers=None, from_user=None, **kwargs): # required by Mastodon # https://github.com/tootsuite/mastodon/pull/14556#issuecomment-674077648 'Host': util.domain_from_link(url, minimize=False), - 'Content-Type': as2.CONTENT_TYPE, + 'Content-Type': as2.CONTENT_TYPE_LD_PROFILE, # required for HTTP Signature and Mastodon 'Digest': f'SHA-256={b64encode(sha256(data or b"").digest()).decode()}', } @@ -849,7 +849,7 @@ def actor(handle_or_id): logger.info(f'Returning: {json_dumps(actor, indent=2)}') return actor, { - 'Content-Type': as2.CONTENT_TYPE, + 'Content-Type': as2.CONTENT_TYPE_LD_PROFILE, 'Access-Control-Allow-Origin': '*', } @@ -952,7 +952,7 @@ def follower_collection(id, collection): return f'{protocol} user {id} not found', 404 if request.method == 'HEAD': - return '', {'Content-Type': as2.CONTENT_TYPE} + return '', {'Content-Type': as2.CONTENT_TYPE_LD_PROFILE} # page followers, new_before, new_after = Follower.fetch_page(collection, user=user) @@ -973,7 +973,7 @@ def follower_collection(id, collection): 'id': request.url, }) logger.info(f'Returning {json_dumps(page, indent=2)}') - return page, {'Content-Type': as2.CONTENT_TYPE} + return page, {'Content-Type': as2.CONTENT_TYPE_LD_PROFILE} # collection num_followers, num_following = user.count_followers() @@ -986,7 +986,7 @@ def follower_collection(id, collection): 'first': page, } logger.info(f'Returning {json_dumps(collection, indent=2)}') - return collection, {'Content-Type': as2.CONTENT_TYPE} + return collection, {'Content-Type': as2.CONTENT_TYPE_LD_PROFILE} # protocol in subdomain @@ -1010,7 +1010,7 @@ def outbox(id): error(f'User {id} not found', status=404) if request.method == 'HEAD': - return '', {'Content-Type': as2.CONTENT_TYPE} + return '', {'Content-Type': as2.CONTENT_TYPE_LD_PROFILE} query = Object.query(Object.users == user.key) objects, new_before, new_after = fetch_objects(query, by=Object.updated, @@ -1034,7 +1034,7 @@ def outbox(id): 'id': request.url, }) logger.info(f'Returning {json_dumps(page, indent=2)}') - return page, {'Content-Type': as2.CONTENT_TYPE} + return page, {'Content-Type': as2.CONTENT_TYPE_LD_PROFILE} # collection return { @@ -1044,4 +1044,4 @@ def outbox(id): 'summary': f"{id}'s outbox", 'totalItems': query.count(), 'first': page, - }, {'Content-Type': as2.CONTENT_TYPE} + }, {'Content-Type': as2.CONTENT_TYPE_LD_PROFILE} diff --git a/redirect.py b/redirect.py index d6da2c4..e8e8bc6 100644 --- a/redirect.py +++ b/redirect.py @@ -109,7 +109,9 @@ def redir(to): ret = ActivityPub.convert(obj, from_user=user) logger.info(f'Returning: {json_dumps(ret, indent=2)}') return ret, { - 'Content-Type': accept_type, + 'Content-Type': (as2.CONTENT_TYPE_LD_PROFILE + if accept_type == as2.CONTENT_TYPE_LD + else accept_type), 'Access-Control-Allow-Origin': '*', **VARY_HEADER, } diff --git a/tests/test_activitypub.py b/tests/test_activitypub.py index 223e231..633b054 100644 --- a/tests/test_activitypub.py +++ b/tests/test_activitypub.py @@ -357,8 +357,7 @@ class ActivityPubTest(TestCase): self.make_user('fake:user', cls=Fake) got = self.client.get('/ap/fake:user', base_url='https://fa.brid.gy/') self.assertEqual(200, got.status_code, got.get_data(as_text=True)) - type = got.headers['Content-Type'] - self.assertTrue(type.startswith(as2.CONTENT_TYPE), type) + self.assertEqual(as2.CONTENT_TYPE_LD_PROFILE, got.headers['Content-Type']) self.assertEqual(ACTOR_FAKE, got.json) def test_actor_fake_protocol_subdomain(self, *_): @@ -371,8 +370,7 @@ class ActivityPubTest(TestCase): """Web users are special cased to drop the /web/ prefix.""" got = self.client.get('/user.com') self.assertEqual(200, got.status_code) - type = got.headers['Content-Type'] - self.assertTrue(type.startswith(as2.CONTENT_TYPE), type) + self.assertEqual(as2.CONTENT_TYPE_LD_PROFILE, got.headers['Content-Type']) self.assertEqual({ **ACTOR_BASE, 'type': 'Person', diff --git a/tests/test_redirect.py b/tests/test_redirect.py index a201c3e..3eefaec 100644 --- a/tests/test_redirect.py +++ b/tests/test_redirect.py @@ -79,7 +79,7 @@ class RedirectTest(testutil.TestCase): self._test_as2(as2.CONTENT_TYPE) def test_as2_ld(self): - self._test_as2(as2.CONTENT_TYPE_LD) + self._test_as2(as2.CONTENT_TYPE_LD_PROFILE) def test_as2_creates_user(self): Object(id='https://user.com/repost', as2=REPOST_AS2).put() @@ -87,7 +87,7 @@ class RedirectTest(testutil.TestCase): self.user.key.delete() resp = self.client.get('/r/https://user.com/repost', - headers={'Accept': as2.CONTENT_TYPE}) + headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE}) self.assertEqual(200, resp.status_code, resp.get_data(as_text=True)) self.assert_equals(REPOST_AS2, resp.json) self.assertEqual('Accept', resp.headers['Vary']) @@ -102,7 +102,7 @@ class RedirectTest(testutil.TestCase): ] resp = self.client.get('/r/https://user.com/repost', - headers={'Accept': as2.CONTENT_TYPE}) + headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE}) self.assertEqual(200, resp.status_code, resp.get_data(as_text=True)) self.assert_equals(REPOST_AS2, resp.json) self.assertEqual('Accept', resp.headers['Vary']) @@ -116,7 +116,7 @@ class RedirectTest(testutil.TestCase): ] resp = self.client.get('/r/https://user.com/repost', - headers={'Accept': as2.CONTENT_TYPE}) + headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE}) self.assertEqual(200, resp.status_code, resp.get_data(as_text=True)) self.assert_equals(REPOST_AS2, resp.json) self.assertEqual('Accept', resp.headers['Vary']) @@ -129,7 +129,7 @@ class RedirectTest(testutil.TestCase): protocol.objects_cache.clear() resp = self.client.get('/r/https://user.com/', - headers={'Accept': as2.CONTENT_TYPE}) + headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE}) self.assertEqual(200, resp.status_code, resp.get_data(as_text=True)) self.assertEqual('Accept', resp.headers['Vary']) @@ -157,7 +157,7 @@ class RedirectTest(testutil.TestCase): cache.init_app(app) self.client = app.test_client() - self._test_as2(as2.CONTENT_TYPE) + self._test_as2(as2.CONTENT_TYPE_LD_PROFILE) resp = self.client.get('/r/https://user.com/bar') self.assertEqual(301, resp.status_code) @@ -166,7 +166,7 @@ class RedirectTest(testutil.TestCase): # delete stored Object to make sure we're serving from cache self.obj.delete() - self._test_as2(as2.CONTENT_TYPE) + self._test_as2(as2.CONTENT_TYPE_LD_PROFILE) resp = self.client.get('/r/https://user.com/bar', headers={'Accept': 'text/html'}) @@ -186,7 +186,7 @@ class RedirectTest(testutil.TestCase): Object(id='https://user.com/bar', as2={}, deleted=True).put() resp = self.client.get('/r/https://user.com/bar', - headers={'Accept': as2.CONTENT_TYPE}) + headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE}) self.assertEqual(404, resp.status_code, resp.get_data(as_text=True)) def test_as2_opted_out(self): @@ -194,5 +194,5 @@ class RedirectTest(testutil.TestCase): self.user.put() resp = self.client.get('/r/https://user.com/', - headers={'Accept': as2.CONTENT_TYPE}) + headers={'Accept': as2.CONTENT_TYPE_LD_PROFILE}) self.assertEqual(404, resp.status_code, resp.get_data(as_text=True)) diff --git a/tests/test_web.py b/tests/test_web.py index 910503e..54283e6 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -428,7 +428,8 @@ class WebTest(TestCase): calls = {} # maps inbox URL to JSON data for args, kwargs in mock_post.call_args_list: - self.assertEqual(as2.CONTENT_TYPE, kwargs['headers']['Content-Type']) + self.assertEqual(as2.CONTENT_TYPE_LD_PROFILE, + kwargs['headers']['Content-Type']) rsa_key = kwargs['auth'].header_signer._rsa._key self.assertEqual(self.user.private_pem(), rsa_key.exportKey()) calls[args[0]] = json_loads(kwargs['data']) diff --git a/tests/test_webfinger.py b/tests/test_webfinger.py index 4dcb6cb..bcf78a6 100644 --- a/tests/test_webfinger.py +++ b/tests/test_webfinger.py @@ -3,6 +3,7 @@ import copy from unittest.mock import patch import urllib.parse +from granary.as2 import CONTENT_TYPE_LD_PROFILE from oauth_dropins.webutil.testutil import requests_response # import first so that Fake is defined before URL routes are registered @@ -39,15 +40,15 @@ WEBFINGER = { 'href': 'https://user.com/about-me', }, { 'rel': 'self', - 'type': 'application/activity+json', + 'type': CONTENT_TYPE_LD_PROFILE, 'href': 'http://localhost/user.com', }, { 'rel': 'inbox', - 'type': 'application/activity+json', + 'type': CONTENT_TYPE_LD_PROFILE, 'href': 'http://localhost/user.com/inbox' }, { 'rel': 'sharedInbox', - 'type': 'application/activity+json', + 'type': CONTENT_TYPE_LD_PROFILE, 'href': 'https://web.brid.gy/ap/sharedInbox', }, { 'rel': 'http://ostatus.org/schema/1.0/subscribe', @@ -68,15 +69,15 @@ WEBFINGER_NO_HCARD = { 'href': 'https://user.com/', }, { 'rel': 'self', - 'type': 'application/activity+json', + 'type': CONTENT_TYPE_LD_PROFILE, 'href': 'https://web.brid.gy/user.com', }, { 'rel': 'inbox', - 'type': 'application/activity+json', + 'type': CONTENT_TYPE_LD_PROFILE, 'href': 'https://web.brid.gy/user.com/inbox', }, { 'rel': 'sharedInbox', - 'type': 'application/activity+json', + 'type': CONTENT_TYPE_LD_PROFILE, 'href': 'https://web.brid.gy/ap/sharedInbox', }, { 'rel': 'http://ostatus.org/schema/1.0/subscribe', @@ -92,15 +93,15 @@ WEBFINGER_FAKE = { 'href': 'fake:user', }, { 'rel': 'self', - 'type': 'application/activity+json', + 'type': CONTENT_TYPE_LD_PROFILE, 'href': 'http://localhost/ap/fa/fake:user', }, { 'rel': 'inbox', - 'type': 'application/activity+json', + 'type': CONTENT_TYPE_LD_PROFILE, 'href': 'http://localhost/ap/fa/fake:user/inbox', }, { 'rel': 'sharedInbox', - 'type': 'application/activity+json', + 'type': CONTENT_TYPE_LD_PROFILE, 'href': 'https://web.brid.gy/ap/sharedInbox', }, { 'rel': 'http://ostatus.org/schema/1.0/subscribe', diff --git a/tests/testutil.py b/tests/testutil.py index b12144a..b73396c 100644 --- a/tests/testutil.py +++ b/tests/testutil.py @@ -392,7 +392,7 @@ class TestCase(unittest.TestCase, testutil.Asserts): headers = { 'Date': 'Sun, 02 Jan 2022 03:04:05 GMT', 'Host': util.domain_from_link(url, minimize=False), - 'Content-Type': 'application/activity+json', + 'Content-Type': as2.CONTENT_TYPE_LD_PROFILE, 'Digest': ANY, **CONNEG_HEADERS_AS2_HTML, **kwargs.pop('headers', {}), diff --git a/webfinger.py b/webfinger.py index 8a732f7..994fbf4 100644 --- a/webfinger.py +++ b/webfinger.py @@ -138,7 +138,7 @@ class Webfinger(flask_util.XrdOrJrd): # ActivityPub { 'rel': 'self', - 'type': as2.CONTENT_TYPE, + 'type': as2.CONTENT_TYPE_LD_PROFILE, # 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. @@ -147,12 +147,12 @@ class Webfinger(flask_util.XrdOrJrd): # AP reads this and sharedInbox from the AS2 actor, not # webfinger, so strictly speaking, it's probably not needed here. 'rel': 'inbox', - 'type': as2.CONTENT_TYPE, + 'type': as2.CONTENT_TYPE_LD_PROFILE, 'href': actor_id + '/inbox', }, { # https://www.w3.org/TR/activitypub/#sharedInbox 'rel': 'sharedInbox', - 'type': as2.CONTENT_TYPE, + 'type': as2.CONTENT_TYPE_LD_PROFILE, 'href': common.subdomain_wrap(cls, '/ap/sharedInbox'), },