use protocol subdomains in AP inbox

...and other misc protocol subdomain fixes
pull/655/head
Ryan Barrett 2023-09-27 13:55:16 -07:00
rodzic 0b592ace35
commit a823dd1d65
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 6BE31FDF4776E9D4
9 zmienionych plików z 86 dodań i 47 usunięć

Wyświetl plik

@ -714,7 +714,7 @@ def postprocess_as2_actor(actor, wrap=True):
@flask_util.cached(cache, CACHE_TIME)
def actor(handle_or_id):
"""Serves a user's AS2 actor from the datastore."""
cls = Protocol.for_request(fed=PROTOCOLS['web'])
cls = Protocol.for_request(fed='web')
if not cls:
error(f"Couldn't determine protocol", status=404)
@ -771,15 +771,17 @@ def actor(handle_or_id):
}
# note that this path overlaps with the /ap/<handle_or_id> actor route above,
# but doesn't collide because this is POST and that one is GET.
# note that this shared inbox path overlaps with the /ap/<handle_or_id> actor
# route above, but doesn't collide because this is POST and that one is GET.
@app.post('/ap/sharedInbox')
# TODO: protocol in subdomain
# source protocol in subdomain
@app.post(f'/ap/<id>/inbox')
# source protocol in path; primarily for backcompat
@app.post(f'/ap/<any({",".join(PROTOCOLS)}):protocol>/<id>/inbox')
# special case Web users without /ap/web/ prefix, for backward compatibility
@app.post('/inbox')
@app.post(f'/<regex("{DOMAIN_RE}"):id>/inbox')
def inbox(protocol='web', id=None):
@app.post('/inbox', defaults={'protocol': 'web'})
@app.post(f'/<regex("{DOMAIN_RE}"):id>/inbox', defaults={'protocol': 'web'})
def inbox(protocol=None, id=None):
"""Handles ActivityPub inbox delivery."""
# parse and validate AS2 activity
try:
@ -796,13 +798,12 @@ def inbox(protocol='web', id=None):
# load receiving user
obj_id = as1.get_object(redirect_unwrap(activity)).get('id')
to_proto = None
if protocol:
to_proto = PROTOCOLS[protocol]
elif type == 'Follow':
to_proto = PROTOCOLS[protocol] if protocol else Protocol.for_request(fed='web')
if not to_proto and type == 'Follow':
to_proto = Protocol.for_id(obj_id)
if to_proto:
logger.info(f'To protocol {to_proto.LABEL}')
to_user_id = None
if id:
to_user_id = id
@ -860,7 +861,7 @@ def follower_collection(id, collection):
* https://www.w3.org/TR/activitypub/#collections
* https://www.w3.org/TR/activitystreams-core/#paging
"""
protocol = Protocol.for_request(fed=PROTOCOLS['web'])
protocol = Protocol.for_request(fed='web')
assert protocol
g.user = protocol.get_by_id(id)
if not g.user:
@ -912,7 +913,7 @@ def follower_collection(id, collection):
# special case Web users without /ap/web/ prefix, for backward compatibility
@app.get(f'/<regex("{DOMAIN_RE}"):id>/outbox')
def outbox(id):
protocol = Protocol.for_request(fed=PROTOCOLS['web'])
protocol = Protocol.for_request(fed='web')
assert protocol
return {
'@context': 'https://www.w3.org/ns/activitystreams',

Wyświetl plik

@ -36,24 +36,26 @@ PRIMARY_DOMAIN = 'fed.brid.gy'
SUPERDOMAIN = '.brid.gy'
# TODO: add a Flask route decorator version of util.canonicalize_domain, then
# use it to canonicalize most UI routes from these to fed.brid.gy.
OTHER_DOMAINS = (
PROTOCOL_DOMAINS = (
'ap.brid.gy',
'atp.brid.gy',
'atproto.brid.gy',
'bluesky.brid.gy',
'bsky.brid.gy',
'bridgy-federated.appspot.com',
'bridgy-federated.uc.r.appspot.com',
'fa.brid.gy',
'nostr.brid.gy',
'web.brid.gy',
)
OTHER_DOMAINS = (
'bridgy-federated.appspot.com',
'bridgy-federated.uc.r.appspot.com',
)
LOCAL_DOMAINS = (
'localhost',
'localhost:8080',
'my.dev.com:8080',
)
DOMAINS = (PRIMARY_DOMAIN,) + OTHER_DOMAINS + LOCAL_DOMAINS
DOMAINS = (PRIMARY_DOMAIN,) + PROTOCOL_DOMAINS + OTHER_DOMAINS + LOCAL_DOMAINS
# TODO: unify with Bridgy's
DOMAIN_BLOCKLIST = (
# https://github.com/snarfed/bridgy-fed/issues/348
@ -96,6 +98,7 @@ def host_url(path_query=None):
or (not DEBUG and request.host in LOCAL_DOMAINS)):
base = f'https://{PRIMARY_DOMAIN}'
assert base
return urllib.parse.urljoin(base, path_query)

Wyświetl plik

@ -74,12 +74,13 @@ class Protocol:
...based on the request's hostname.
Args:
fed: :class:`Protocol` subclass to return if the current request is on
fed.brid.gy
fed (str or Protocol): protocol to return if the current request is on
``fed.brid.gy``
Returns:
:class:`Protocol` subclass, or None if the provided domain or request
hostname domain is not a subdomain of brid.gy or isn't a known protocol
Protocol subclass: ...or None if the provided domain or request
hostname domain is not a subdomain of ``brid.gy` or isn't a known
protocol
"""
return Protocol.for_bridgy_subdomain(request.host, fed=fed)
@ -89,19 +90,20 @@ class Protocol:
Args:
domain_or_url: str
fed: :class:`Protocol` subclass to return if the domain_or_url is on
fed.brid.gy
fed (str or Protocol): protocol to return if the current request is on
``fed.brid.gy``
Returns:
:class:`Protocol` subclass, or None if the request hostname is not a
subdomain of brid.gy or isn't a known protocol
Protocol subclass: ...or None if the provided domain or request
hostname domain is not a subdomain of ``brid.gy` or isn't a known
protocol
"""
domain = (util.domain_from_link(domain_or_url, minimize=False)
if util.is_web(domain_or_url)
else domain_or_url)
if domain == common.PRIMARY_DOMAIN or domain in common.LOCAL_DOMAINS:
return fed
return PROTOCOLS[fed] if isinstance(fed, str) else fed
elif domain and domain.endswith(common.SUPERDOMAIN):
label = domain.removesuffix(common.SUPERDOMAIN)
return PROTOCOLS.get(label)

Wyświetl plik

@ -13,7 +13,8 @@ from google.cloud import ndb
from granary import as2, microformats2
from httpsig import HeaderSigner
from oauth_dropins.webutil.testutil import requests_response
from oauth_dropins.webutil.util import json_dumps, json_loads
from oauth_dropins.webutil.util import domain_from_link, json_dumps, json_loads
from oauth_dropins.webutil import util
import requests
from urllib3.exceptions import ReadTimeoutError
from werkzeug.exceptions import BadGateway
@ -232,11 +233,11 @@ ACCEPT = {
}
UNDO_FOLLOW_WRAPPED = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://mas.to/6d1b',
'type': 'Undo',
'actor': 'https://mas.to/users/swentel',
'object': FOLLOW_WRAPPED,
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://mas.to/6d1b',
'type': 'Undo',
'actor': 'https://mas.to/users/swentel',
'object': FOLLOW_WRAPPED,
}
DELETE = {
@ -318,12 +319,12 @@ class ActivityPubTest(TestCase):
props.setdefault('delivered_protocol', 'web')
return super().assert_object(id, **props)
def sign(self, path, body):
def sign(self, path, body, host=None):
"""Constructs HTTP Signature, returns headers."""
digest = b64encode(sha256(body.encode()).digest()).decode()
headers = {
'Date': 'Sun, 02 Jan 2022 03:04:05 GMT',
'Host': 'localhost',
'Host': host or 'localhost',
'Content-Type': as2.CONTENT_TYPE,
'Digest': f'SHA-256={digest}',
}
@ -332,10 +333,13 @@ class ActivityPubTest(TestCase):
headers=('Date', 'Host', 'Digest', '(request-target)'))
return hs.sign(headers, method='POST', path=path)
def post(self, path, json=None):
def post(self, path, json=None, base_url=None, **kwargs):
"""Wrapper around self.client.post that adds signature."""
body = json_dumps(json)
return self.client.post(path, data=body, headers=self.sign(path, body))
host = domain_from_link(base_url) if base_url else None
headers = self.sign(path, body, host=host)
return self.client.post(path, data=body, headers=headers,
base_url=base_url, **kwargs)
def test_actor_fake(self, *_):
self.make_user('fake:user', cls=Fake)
@ -541,6 +545,24 @@ class ActivityPubTest(TestCase):
},
)
def test_inbox_reply_protocol_subdomain(self, reply, *_):
Fake.fetchable['fake:post'] = as2.to_as1({
**NOTE_OBJECT,
'id': 'fake:post',
})
reply = {
**REPLY_OBJECT,
'id': 'fake:my-reply',
'inReplyTo': 'fake:post',
}
got = self.post('/ap/fake:user/inbox', json=reply,
base_url='https://fa.brid.gy/')
self.assertEqual(200, got.status_code)
[(obj, target)] = Fake.sent
self.assertEqual('fake:my-reply#bridgy-fed-create', obj.our_as1['id'])
self.assertEqual('fake:post:target', target)
def test_inbox_reply_to_self_domain(self, *mocks):
self._test_inbox_ignore_reply_to('http://localhost/mas.to', *mocks)

Wyświetl plik

@ -101,3 +101,6 @@ class CommonTest(TestCase):
with app.test_request_context(base_url='http://bridgy-federated.uc.r.appspot.com'):
self.assertEqual('https://fed.brid.gy/asdf', common.host_url('asdf'))
with app.test_request_context(base_url='https://atproto.brid.gy', path='/foo'):
self.assertEqual('https://atproto.brid.gy/asdf', common.host_url('asdf'))

Wyświetl plik

@ -22,6 +22,11 @@ COMMENT_AS2 = {
'name': 'A ☕ reply',
'inReplyTo': 'https://fake.com/123',
}
COMMENT_AS2_WEB = {
**COMMENT_AS2,
'id': 'https://web.brid.gy/r/tag:fake.com:123456',
'url': 'https://web.brid.gy/r/https://fake.com/123456',
}
HTML = """\
<!DOCTYPE html>
<html>
@ -248,7 +253,7 @@ A ☕ reply
resp = self.client.get(f'/convert/ap/{url}',
base_url='https://web.brid.gy/')
self.assertEqual(200, resp.status_code)
self.assert_equals(COMMENT_AS2, resp.json, ignore=['to'])
self.assert_equals(COMMENT_AS2_WEB, resp.json, ignore=['to'])
@patch('requests.get')
def test_web_to_activitypub_fetch(self, mock_get):
@ -261,7 +266,7 @@ A ☕ reply
resp = self.client.get(f'/convert/ap/{url}',
base_url='https://web.brid.gy/')
self.assertEqual(200, resp.status_code)
self.assert_equals(COMMENT_AS2, resp.json, ignore=['to'])
self.assert_equals(COMMENT_AS2_WEB, resp.json, ignore=['to'])
def test_web_to_activitypub_no_user(self):
resp = self.client.get(f'/convert/ap/http://nope.com/post',
@ -276,7 +281,7 @@ A ☕ reply
resp = self.client.get(f'/convert/ap/http://user.com/a%23b',
base_url='https://web.brid.gy/')
self.assertEqual(200, resp.status_code)
self.assert_equals(COMMENT_AS2, resp.json, ignore=['to'])
self.assert_equals(COMMENT_AS2_WEB, resp.json, ignore=['to'])
def test_fed_subdomain(self):
url = 'https://user.com/post'

Wyświetl plik

@ -74,6 +74,8 @@ class ProtocolTest(TestCase):
with self.subTest(url=url, expected=expected):
self.assertEqual(expected,
Protocol.for_bridgy_subdomain(url, fed=Fake))
self.assertEqual(expected,
Protocol.for_bridgy_subdomain(url, fed='fake'))
with app.test_request_context('/foo', base_url=url):
self.assertEqual(expected, Protocol.for_request(fed=Fake))

Wyświetl plik

@ -49,7 +49,7 @@ WEBFINGER = {
'href': 'https://web.brid.gy/ap/sharedInbox',
}, {
'rel': 'http://ostatus.org/schema/1.0/subscribe',
'template': 'http://localhost/web/user.com?url={uri}',
'template': 'https://fed.brid.gy/web/user.com?url={uri}',
}],
}
WEBFINGER_NO_HCARD = {
@ -77,7 +77,7 @@ WEBFINGER_NO_HCARD = {
'href': 'https://web.brid.gy/ap/sharedInbox',
}, {
'rel': 'http://ostatus.org/schema/1.0/subscribe',
'template': 'http://localhost/web/user.com?url={uri}',
'template': 'https://fed.brid.gy/web/user.com?url={uri}',
}],
}
WEBFINGER_FAKE = {
@ -101,7 +101,7 @@ WEBFINGER_FAKE = {
'href': 'https://web.brid.gy/ap/sharedInbox',
}, {
'rel': 'http://ostatus.org/schema/1.0/subscribe',
'template': 'http://localhost/fa/fake:user?url={uri}',
'template': 'https://fed.brid.gy/fa/fake:handle:user?url={uri}',
}],
}
WEBFINGER_FAKE_FA_BRID_GY = copy.deepcopy(WEBFINGER_FAKE)
@ -109,7 +109,7 @@ for link in WEBFINGER_FAKE_FA_BRID_GY['links']:
if 'href' in link:
link['href'] = link['href'].replace('http://localhost/ap/fa', 'https://fa.brid.gy/ap')
WEBFINGER_FAKE_FA_BRID_GY['links'][3]['href'] = 'https://fa.brid.gy/ap/sharedInbox'
WEBFINGER_FAKE_FA_BRID_GY['links'][4]['template'] = 'https://fed.brid.gy/fa/fake:user?url={uri}'
WEBFINGER_FAKE_FA_BRID_GY['links'][4]['template'] = 'https://fed.brid.gy/fa/fake:handle:user?url={uri}'
class HostMetaTest(TestCase):

Wyświetl plik

@ -15,7 +15,6 @@ from oauth_dropins.webutil.util import json_dumps, json_loads
import common
from flask_app import app, cache
from protocol import Protocol
from web import Web
SUBSCRIBE_LINK_REL = 'http://ostatus.org/schema/1.0/subscribe'
@ -50,7 +49,7 @@ class Webfinger(flask_util.XrdOrJrd):
cls = None
try:
user, id = util.parse_acct_uri(resource)
cls = Protocol.for_bridgy_subdomain(id, fed=Web)
cls = Protocol.for_bridgy_subdomain(id, fed='web')
if cls:
id = user
allow_indirect = True
@ -58,7 +57,7 @@ class Webfinger(flask_util.XrdOrJrd):
id = urllib.parse.urlparse(resource).netloc or resource
if not cls:
cls = Protocol.for_request(fed=Web)
cls = Protocol.for_request(fed='web')
if cls.owns_id(id) is False:
logger.info(f'{id} is not a {cls.LABEL} id')
@ -142,11 +141,13 @@ class Webfinger(flask_util.XrdOrJrd):
# https://github.com/snarfed/bridgy-fed/issues/60#issuecomment-1325589750
{
'rel': 'http://ostatus.org/schema/1.0/subscribe',
# always use fed.brid.gy for UI pages, not protocol subdomain
# TODO: switch to:
# 'template': common.host_url(g.user.user_page_path('?url={uri}')),
# the problem is that user_page_path() uses handle_or_id, which uses
# custom username instead of domain, which may not be unique
'template': common.host_url(f'{cls.ABBREV}/{id}?url={{uri}}'),
'template': f'https://{common.PRIMARY_DOMAIN}' +
g.user.user_page_path('?url={uri}'),
}]
})