kopia lustrzana https://github.com/snarfed/bridgy-fed
use protocol subdomains in AP inbox
...and other misc protocol subdomain fixespull/655/head
rodzic
0b592ace35
commit
a823dd1d65
|
@ -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',
|
||||
|
|
11
common.py
11
common.py
|
@ -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)
|
||||
|
||||
|
||||
|
|
20
protocol.py
20
protocol.py
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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}'),
|
||||
}]
|
||||
})
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue