kopia lustrzana https://github.com/snarfed/bridgy-fed
rodzic
717b068193
commit
4b37674624
33
common.py
33
common.py
|
@ -71,6 +71,7 @@ DOMAIN_BLOCKLIST = frozenset((
|
|||
'twitter.com',
|
||||
) + DOMAINS)
|
||||
|
||||
# currently unused. TODO: remove?
|
||||
_DEFAULT_SIGNATURE_USER = None
|
||||
|
||||
CACHE_TIME = timedelta(seconds=60)
|
||||
|
@ -87,6 +88,7 @@ def host_url(path_query=None):
|
|||
return urllib.parse.urljoin(base, path_query)
|
||||
|
||||
|
||||
# currently unused. TODO: remove?
|
||||
def default_signature_user():
|
||||
global _DEFAULT_SIGNATURE_USER
|
||||
if _DEFAULT_SIGNATURE_USER is None:
|
||||
|
@ -94,38 +96,35 @@ def default_signature_user():
|
|||
return _DEFAULT_SIGNATURE_USER
|
||||
|
||||
|
||||
def signed_get(url, **kwargs):
|
||||
return signed_request(util.requests_get, url, **kwargs)
|
||||
def signed_get(url, user, **kwargs):
|
||||
return signed_request(util.requests_get, url, user, **kwargs)
|
||||
|
||||
|
||||
def signed_post(url, **kwargs):
|
||||
return signed_request(util.requests_post, url, **kwargs)
|
||||
def signed_post(url, user, **kwargs):
|
||||
return signed_request(util.requests_post, url, user, **kwargs)
|
||||
|
||||
|
||||
def signed_request(fn, url, data=None, log_data=True, user=None, headers=None, **kwargs):
|
||||
def signed_request(fn, url, user, data=None, log_data=True, headers=None, **kwargs):
|
||||
"""Wraps requests.* and adds HTTP Signature.
|
||||
|
||||
Args:
|
||||
fn: :func:`util.requests_get` or :func:`util.requests_get`
|
||||
url: str
|
||||
user: :class:`User` to sign request with
|
||||
data: optional AS2 object
|
||||
log_data: boolean, whether to log full data object
|
||||
user: optional :class:`User` to sign request with
|
||||
kwargs: passed through to requests
|
||||
|
||||
Returns: :class:`requests.Response`
|
||||
"""
|
||||
assert user
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
||||
# prepare HTTP Signature and headers
|
||||
if not user:
|
||||
user = default_signature_user()
|
||||
|
||||
if data:
|
||||
if log_data:
|
||||
logging.info(f'Sending AS2 object: {json_dumps(data, indent=2)}')
|
||||
data = kwargs['data'] = json_dumps(data).encode()
|
||||
data = json_dumps(data).encode()
|
||||
|
||||
headers = copy.deepcopy(headers)
|
||||
headers.update({
|
||||
|
@ -154,13 +153,15 @@ def signed_request(fn, url, data=None, log_data=True, user=None, headers=None, *
|
|||
|
||||
# make HTTP request
|
||||
kwargs.setdefault('gateway', True)
|
||||
resp = fn(url, auth=auth, headers=headers, allow_redirects=False, **kwargs)
|
||||
|
||||
resp = fn(url, data=data, auth=auth, headers=headers, allow_redirects=False,
|
||||
**kwargs)
|
||||
logger.info(f'Got {resp.status_code} headers: {resp.headers}')
|
||||
|
||||
# handle GET redirects manually so that we generate a new HTTP signature
|
||||
if resp.is_redirect and fn == util.requests_get:
|
||||
return signed_request(fn, resp.headers['Location'], data=data, user=user,
|
||||
headers=headers, **kwargs)
|
||||
headers=headers, log_data=log_data, **kwargs)
|
||||
|
||||
type = content_type(resp)
|
||||
if (type and type != 'text/html' and
|
||||
(type.startswith('text/') or type.endswith('+json') or type.endswith('/json'))):
|
||||
|
@ -199,6 +200,8 @@ def get_as2(url, user=None):
|
|||
If we raise a werkzeug HTTPException, it will have an additional
|
||||
requests_response attribute with the last requests.Response we received.
|
||||
"""
|
||||
assert user
|
||||
|
||||
def _error(resp):
|
||||
msg = f"Couldn't fetch {url} as ActivityStreams 2"
|
||||
logger.warning(msg)
|
||||
|
@ -217,7 +220,7 @@ def get_as2(url, user=None):
|
|||
_error(resp)
|
||||
|
||||
resp = signed_get(urllib.parse.urljoin(resp.url, obj['href']),
|
||||
headers=as2.CONNEG_HEADERS)
|
||||
user=user, headers=as2.CONNEG_HEADERS)
|
||||
if content_type(resp) in (as2.CONTENT_TYPE, CONTENT_TYPE_LD_PLAIN):
|
||||
return resp
|
||||
|
||||
|
|
|
@ -147,7 +147,7 @@ class FollowCallback(indieauth.Callback):
|
|||
flash(f"Couldn't find ActivityPub profile link for {addr}")
|
||||
return redirect(f'/user/{domain}/following')
|
||||
|
||||
resp = common.get_as2(as2_url)
|
||||
resp = common.get_as2(as2_url, user=user)
|
||||
followee = resp.json()
|
||||
id = followee.get('id')
|
||||
inbox = followee.get('inbox')
|
||||
|
@ -165,7 +165,7 @@ class FollowCallback(indieauth.Callback):
|
|||
'actor': common.host_url(domain),
|
||||
'to': [as2.PUBLIC_AUDIENCE],
|
||||
}
|
||||
common.signed_post(inbox, data=follow_as2)
|
||||
common.signed_post(inbox, user=user, data=follow_as2)
|
||||
|
||||
follow_json = json_dumps(follow_as2, sort_keys=True)
|
||||
Follower.get_or_create(dest=id, src=domain, status='active',
|
||||
|
@ -218,7 +218,7 @@ class UnfollowCallback(indieauth.Callback):
|
|||
if isinstance(followee, str):
|
||||
# fetch as AS2 to get full followee with inbox
|
||||
followee_id = followee
|
||||
resp = common.get_as2(followee_id)
|
||||
resp = common.get_as2(followee_id, user=user)
|
||||
followee = resp.json()
|
||||
|
||||
inbox = followee.get('inbox')
|
||||
|
@ -235,7 +235,7 @@ class UnfollowCallback(indieauth.Callback):
|
|||
'actor': common.host_url(domain),
|
||||
'object': last_follow,
|
||||
}
|
||||
common.signed_post(inbox, data=unfollow_as2)
|
||||
common.signed_post(inbox, user=user, data=unfollow_as2)
|
||||
|
||||
follower.status = 'inactive'
|
||||
follower.put()
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
from unittest import mock
|
||||
|
||||
from granary import as2
|
||||
from oauth_dropins.webutil import util
|
||||
from oauth_dropins.webutil import appengine_config, util
|
||||
from oauth_dropins.webutil.testutil import requests_response
|
||||
import requests
|
||||
from werkzeug.exceptions import BadGateway
|
||||
|
@ -31,6 +31,12 @@ NOT_ACCEPTABLE = requests_response(status=406)
|
|||
|
||||
|
||||
class CommonTest(testutil.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
with appengine_config.ndb_client.context():
|
||||
# do this in setUpClass since generating RSA keys is slow
|
||||
cls.user = User.get_or_create('site')
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.app_context = app.test_request_context('/')
|
||||
|
@ -42,7 +48,7 @@ class CommonTest(testutil.TestCase):
|
|||
|
||||
@mock.patch('requests.get', return_value=AS2)
|
||||
def test_get_as2_direct(self, mock_get):
|
||||
resp = common.get_as2('http://orig')
|
||||
resp = common.get_as2('http://orig', user=self.user)
|
||||
self.assertEqual(AS2, resp)
|
||||
mock_get.assert_has_calls((
|
||||
self.as2_req('http://orig'),
|
||||
|
@ -50,7 +56,7 @@ class CommonTest(testutil.TestCase):
|
|||
|
||||
@mock.patch('requests.get', side_effect=[HTML_WITH_AS2, AS2])
|
||||
def test_get_as2_via_html(self, mock_get):
|
||||
resp = common.get_as2('http://orig')
|
||||
resp = common.get_as2('http://orig', user=self.user)
|
||||
self.assertEqual(AS2, resp)
|
||||
mock_get.assert_has_calls((
|
||||
self.as2_req('http://orig'),
|
||||
|
@ -60,17 +66,17 @@ class CommonTest(testutil.TestCase):
|
|||
@mock.patch('requests.get', return_value=HTML)
|
||||
def test_get_as2_only_html(self, mock_get):
|
||||
with self.assertRaises(BadGateway):
|
||||
resp = common.get_as2('http://orig')
|
||||
resp = common.get_as2('http://orig', user=self.user)
|
||||
|
||||
@mock.patch('requests.get', return_value=NOT_ACCEPTABLE)
|
||||
def test_get_as2_not_acceptable(self, mock_get):
|
||||
with self.assertRaises(BadGateway):
|
||||
resp = common.get_as2('http://orig')
|
||||
resp = common.get_as2('http://orig', user=self.user)
|
||||
|
||||
@mock.patch('requests.get', side_effect=requests.exceptions.SSLError)
|
||||
def test_get_ssl_error(self, mock_get):
|
||||
with self.assertRaises(BadGateway):
|
||||
resp = common.get_as2('http://orig')
|
||||
resp = common.get_as2('http://orig', user=self.user)
|
||||
|
||||
def test_redirect_wrap_empty(self):
|
||||
self.assertIsNone(common.redirect_wrap(None))
|
||||
|
@ -97,7 +103,7 @@ class CommonTest(testutil.TestCase):
|
|||
}, common.postprocess_as2({
|
||||
'id': 'xyz',
|
||||
'inReplyTo': ['foo', 'bar'],
|
||||
}, user=User(id='foo.com')))
|
||||
}, user=User(id='site')))
|
||||
|
||||
def test_postprocess_as2_multiple_url(self):
|
||||
with app.test_request_context('/'):
|
||||
|
@ -108,7 +114,7 @@ class CommonTest(testutil.TestCase):
|
|||
}, common.postprocess_as2({
|
||||
'id': 'xyz',
|
||||
'url': ['foo', 'bar'],
|
||||
}, user=User(id='foo.com')))
|
||||
}, user=User(id='site')))
|
||||
|
||||
def test_postprocess_as2_multiple_image(self):
|
||||
with app.test_request_context('/'):
|
||||
|
@ -120,30 +126,30 @@ class CommonTest(testutil.TestCase):
|
|||
}, common.postprocess_as2({
|
||||
'id': 'xyz',
|
||||
'image': [{'url': 'http://r/foo'}, {'url': 'http://r/bar'}],
|
||||
}, user=User(id='foo.com')))
|
||||
}, user=User(id='site')))
|
||||
|
||||
def test_postprocess_as2_actor_attributedTo(self):
|
||||
with app.test_request_context('/'):
|
||||
self.assert_equals({
|
||||
'actor': {
|
||||
'id': 'baj',
|
||||
'preferredUsername': 'foo.com',
|
||||
'url': 'http://localhost/r/https://foo.com/',
|
||||
'preferredUsername': 'site',
|
||||
'url': 'http://localhost/r/https://site/',
|
||||
},
|
||||
'attributedTo': [{
|
||||
'id': 'bar',
|
||||
'preferredUsername': 'foo.com',
|
||||
'url': 'http://localhost/r/https://foo.com/',
|
||||
'preferredUsername': 'site',
|
||||
'url': 'http://localhost/r/https://site/',
|
||||
}, {
|
||||
'id': 'baz',
|
||||
'preferredUsername': 'foo.com',
|
||||
'url': 'http://localhost/r/https://foo.com/',
|
||||
'preferredUsername': 'site',
|
||||
'url': 'http://localhost/r/https://site/',
|
||||
}],
|
||||
'to': [as2.PUBLIC_AUDIENCE],
|
||||
}, common.postprocess_as2({
|
||||
'attributedTo': [{'id': 'bar'}, {'id': 'baz'}],
|
||||
'actor': {'id': 'baj'},
|
||||
}, user=User(id='foo.com')))
|
||||
}, user=User(id='site')))
|
||||
|
||||
def test_postprocess_as2_note(self):
|
||||
with app.test_request_context('/'):
|
||||
|
@ -152,9 +158,9 @@ class CommonTest(testutil.TestCase):
|
|||
'id': 'http://localhost/r/xyz#bridgy-fed-create',
|
||||
'type': 'Create',
|
||||
'actor': {
|
||||
'id': 'http://localhost/foo.com',
|
||||
'url': 'http://localhost/r/https://foo.com/',
|
||||
'preferredUsername': 'foo.com'
|
||||
'id': 'http://localhost/site',
|
||||
'url': 'http://localhost/r/https://site/',
|
||||
'preferredUsername': 'site'
|
||||
},
|
||||
'object': {
|
||||
'id': 'http://localhost/r/xyz',
|
||||
|
@ -164,7 +170,7 @@ class CommonTest(testutil.TestCase):
|
|||
}, common.postprocess_as2({
|
||||
'id': 'xyz',
|
||||
'type': 'Note',
|
||||
}, user=User(id='foo.com')))
|
||||
}, user=User(id='site')))
|
||||
|
||||
def test_host_url(self):
|
||||
with app.test_request_context():
|
||||
|
@ -187,7 +193,7 @@ class CommonTest(testutil.TestCase):
|
|||
allow_redirects=False),
|
||||
requests_response(status=200, allow_redirects=False),
|
||||
]
|
||||
resp = common.signed_get('https://first')
|
||||
resp = common.signed_get('https://first', user=self.user)
|
||||
|
||||
first = mock_get.call_args_list[0][1]
|
||||
second = mock_get.call_args_list[1][1]
|
||||
|
@ -202,6 +208,6 @@ class CommonTest(testutil.TestCase):
|
|||
requests_response(status=302, redirected_url='http://second',
|
||||
allow_redirects=False),
|
||||
]
|
||||
resp = common.signed_post('https://first')
|
||||
resp = common.signed_post('https://first', user=self.user)
|
||||
mock_post.assert_called_once()
|
||||
self.assertEqual(302, resp.status_code)
|
||||
|
|
|
@ -37,18 +37,18 @@ FOLLOWEE = {
|
|||
FOLLOW_ADDRESS = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'type': 'Follow',
|
||||
'id': f'http://localhost/user/snarfed.org/following#2022-01-02T03:04:05-@foo@bar',
|
||||
'actor': 'http://localhost/snarfed.org',
|
||||
'id': f'http://localhost/user/alice.com/following#2022-01-02T03:04:05-@foo@bar',
|
||||
'actor': 'http://localhost/alice.com',
|
||||
'object': FOLLOWEE,
|
||||
'to': [as2.PUBLIC_AUDIENCE],
|
||||
}
|
||||
FOLLOW_URL = copy.deepcopy(FOLLOW_ADDRESS)
|
||||
FOLLOW_URL['id'] = f'http://localhost/user/snarfed.org/following#2022-01-02T03:04:05-https://bar/actor'
|
||||
FOLLOW_URL['id'] = f'http://localhost/user/alice.com/following#2022-01-02T03:04:05-https://bar/actor'
|
||||
UNDO_FOLLOW = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'type': 'Undo',
|
||||
'id': f'http://localhost/user/snarfed.org/following#undo-2022-01-02T03:04:05-https://bar/id',
|
||||
'actor': 'http://localhost/snarfed.org',
|
||||
'id': f'http://localhost/user/alice.com/following#undo-2022-01-02T03:04:05-https://bar/id',
|
||||
'actor': 'http://localhost/alice.com',
|
||||
'object': FOLLOW_ADDRESS,
|
||||
}
|
||||
|
||||
|
@ -126,7 +126,7 @@ class FollowTest(testutil.TestCase):
|
|||
mock_get.return_value = requests_response('') # IndieAuth endpoint discovery
|
||||
|
||||
resp = self.client.post('/follow/start', data={
|
||||
'me': 'https://snarfed.org',
|
||||
'me': 'https://alice.com',
|
||||
'address': '@foo@bar',
|
||||
})
|
||||
self.assertEqual(302, resp.status_code)
|
||||
|
@ -135,7 +135,7 @@ class FollowTest(testutil.TestCase):
|
|||
|
||||
def test_callback_address(self, mock_get, mock_post):
|
||||
mock_get.side_effect = (
|
||||
# oauth-dropins indieauth https://snarfed.org fetch for user json
|
||||
# oauth-dropins indieauth https://alice.com fetch for user json
|
||||
requests_response(''),
|
||||
WEBFINGER,
|
||||
self.as2_resp(FOLLOWEE),
|
||||
|
@ -153,22 +153,22 @@ class FollowTest(testutil.TestCase):
|
|||
self._test_callback('https://bar/actor', FOLLOW_URL, mock_get, mock_post)
|
||||
|
||||
def _test_callback(self, input, expected_follow, mock_get, mock_post):
|
||||
User.get_or_create('snarfed.org')
|
||||
User.get_or_create('alice.com')
|
||||
|
||||
mock_post.side_effect = (
|
||||
requests_response('me=https://snarfed.org'),
|
||||
requests_response('me=https://alice.com'),
|
||||
requests_response('OK'), # AP Follow to inbox
|
||||
)
|
||||
|
||||
state = util.encode_oauth_state({
|
||||
'endpoint': 'http://auth/endpoint',
|
||||
'me': 'https://snarfed.org',
|
||||
'me': 'https://alice.com',
|
||||
'state': input,
|
||||
})
|
||||
with self.client:
|
||||
resp = self.client.get(f'/follow/callback?code=my_code&state={state}')
|
||||
self.assertEqual(302, resp.status_code)
|
||||
self.assertEqual('/user/snarfed.org/following',resp.headers['Location'])
|
||||
self.assertEqual('/user/alice.com/following',resp.headers['Location'])
|
||||
self.assertEqual([f'Followed <a href="https://bar/url">{input}</a>.'],
|
||||
get_flashed_messages())
|
||||
|
||||
|
@ -180,25 +180,30 @@ class FollowTest(testutil.TestCase):
|
|||
self.assertEqual(('http://bar/inbox',), inbox_args)
|
||||
self.assert_equals(expected_follow, json_loads(inbox_kwargs['data']))
|
||||
|
||||
# check that we signed with the follower's key
|
||||
sig_template = inbox_kwargs['auth'].header_signer.signature_template
|
||||
self.assertTrue(sig_template.startswith('keyId="http://localhost/alice.com"'),
|
||||
sig_template)
|
||||
|
||||
followers = Follower.query().fetch()
|
||||
self.assert_entities_equal(
|
||||
Follower(id='https://bar/id snarfed.org',
|
||||
Follower(id='https://bar/id alice.com',
|
||||
last_follow=json_dumps(expected_follow, sort_keys=True),
|
||||
src='snarfed.org', dest='https://bar/id', status='active'),
|
||||
src='alice.com', dest='https://bar/id', status='active'),
|
||||
followers,
|
||||
ignore=['created', 'updated'])
|
||||
|
||||
id = f'http://localhost/user/snarfed.org/following#2022-01-02T03:04:05-{input}'
|
||||
self.assert_object(id, domains=['snarfed.org'], status='complete',
|
||||
id = f'http://localhost/user/alice.com/following#2022-01-02T03:04:05-{input}'
|
||||
self.assert_object(id, domains=['alice.com'], status='complete',
|
||||
labels=['user', 'activity'], source_protocol='ui',
|
||||
as2=expected_follow, as1=as2.to_as1(expected_follow))
|
||||
|
||||
def test_callback_missing_user(self, mock_get, mock_post):
|
||||
mock_post.return_value = requests_response('me=https://snarfed.org')
|
||||
mock_post.return_value = requests_response('me=https://alice.com')
|
||||
|
||||
state = util.encode_oauth_state({
|
||||
'endpoint': 'http://auth/endpoint',
|
||||
'me': 'https://snarfed.org',
|
||||
'me': 'https://alice.com',
|
||||
'state': '@foo@bar',
|
||||
})
|
||||
with self.client:
|
||||
|
@ -206,46 +211,46 @@ class FollowTest(testutil.TestCase):
|
|||
self.assertEqual(400, resp.status_code)
|
||||
|
||||
def test_callback_user_use_instead(self, mock_get, mock_post):
|
||||
user = User.get_or_create('www.snarfed.org')
|
||||
User.get_or_create('snarfed.org', use_instead=user.key)
|
||||
user = User.get_or_create('www.alice.com')
|
||||
User.get_or_create('alice.com', use_instead=user.key)
|
||||
|
||||
mock_get.side_effect = (
|
||||
requests_response(''),
|
||||
self.as2_resp(FOLLOWEE),
|
||||
)
|
||||
mock_post.side_effect = (
|
||||
requests_response('me=https://snarfed.org'),
|
||||
requests_response('me=https://alice.com'),
|
||||
requests_response('OK'), # AP Follow to inbox
|
||||
)
|
||||
|
||||
state = util.encode_oauth_state({
|
||||
'endpoint': 'http://auth/endpoint',
|
||||
'me': 'https://snarfed.org',
|
||||
'me': 'https://alice.com',
|
||||
'state': 'https://bar/actor',
|
||||
})
|
||||
with self.client:
|
||||
resp = self.client.get(f'/follow/callback?code=my_code&state={state}')
|
||||
self.assertEqual(302, resp.status_code)
|
||||
self.assertEqual('/user/www.snarfed.org/following', resp.headers['Location'])
|
||||
self.assertEqual('/user/www.alice.com/following', resp.headers['Location'])
|
||||
|
||||
id = 'http://localhost/user/www.snarfed.org/following#2022-01-02T03:04:05-https://bar/actor'
|
||||
id = 'http://localhost/user/www.alice.com/following#2022-01-02T03:04:05-https://bar/actor'
|
||||
expected_follow = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'type': 'Follow',
|
||||
'id': id,
|
||||
'actor': 'http://localhost/www.snarfed.org',
|
||||
'actor': 'http://localhost/www.alice.com',
|
||||
'object': FOLLOWEE,
|
||||
'to': [as2.PUBLIC_AUDIENCE],
|
||||
}
|
||||
followers = Follower.query().fetch()
|
||||
self.assert_entities_equal(
|
||||
Follower(id='https://bar/id www.snarfed.org',
|
||||
Follower(id='https://bar/id www.alice.com',
|
||||
last_follow=json_dumps(expected_follow, sort_keys=True),
|
||||
src='www.snarfed.org', dest='https://bar/id', status='active'),
|
||||
src='www.alice.com', dest='https://bar/id', status='active'),
|
||||
followers,
|
||||
ignore=['created', 'updated'])
|
||||
|
||||
self.assert_object(id, domains=['www.snarfed.org'], status='complete',
|
||||
self.assert_object(id, domains=['www.alice.com'], status='complete',
|
||||
labels=['user', 'activity'], source_protocol='ui',
|
||||
as2=expected_follow, as1=as2.to_as1(expected_follow))
|
||||
|
||||
|
@ -257,15 +262,15 @@ class UnfollowTest(testutil.TestCase):
|
|||
def setUp(self):
|
||||
super().setUp()
|
||||
self.follower = Follower(
|
||||
id='https://bar/id snarfed.org', last_follow=json_dumps(FOLLOW_ADDRESS),
|
||||
src='snarfed.org', dest='https://bar/id', status='active',
|
||||
id='https://bar/id alice.com', last_follow=json_dumps(FOLLOW_ADDRESS),
|
||||
src='alice.com', dest='https://bar/id', status='active',
|
||||
).put()
|
||||
|
||||
def test_start(self, mock_get, _):
|
||||
mock_get.return_value = requests_response('') # IndieAuth endpoint discovery
|
||||
|
||||
resp = self.client.post('/unfollow/start', data={
|
||||
'me': 'https://snarfed.org',
|
||||
'me': 'https://alice.com',
|
||||
'key': self.follower.id(),
|
||||
})
|
||||
self.assertEqual(302, resp.status_code)
|
||||
|
@ -284,7 +289,7 @@ class UnfollowTest(testutil.TestCase):
|
|||
})
|
||||
follower.put()
|
||||
|
||||
# oauth-dropins indieauth https://snarfed.org fetch for user json
|
||||
# oauth-dropins indieauth https://alice.com fetch for user json
|
||||
mock_get.side_effect = (
|
||||
requests_response(''),
|
||||
self.as2_resp(FOLLOWEE), # fetch to discover inbox
|
||||
|
@ -296,21 +301,21 @@ class UnfollowTest(testutil.TestCase):
|
|||
self._test_callback(undo, mock_get, mock_post)
|
||||
|
||||
def _test_callback(self, expected_undo, mock_get, mock_post):
|
||||
User.get_or_create('snarfed.org')
|
||||
User.get_or_create('alice.com')
|
||||
mock_post.side_effect = (
|
||||
requests_response('me=https://snarfed.org'),
|
||||
requests_response('me=https://alice.com'),
|
||||
requests_response('OK'), # AP Undo Follow to inbox
|
||||
)
|
||||
|
||||
state = util.encode_oauth_state({
|
||||
'endpoint': 'http://auth/endpoint',
|
||||
'me': 'https://snarfed.org',
|
||||
'me': 'https://alice.com',
|
||||
'state': self.follower.id(),
|
||||
})
|
||||
with self.client:
|
||||
resp = self.client.get(f'/unfollow/callback?code=my_code&state={state}')
|
||||
self.assertEqual(302, resp.status_code)
|
||||
self.assertEqual('/user/snarfed.org/following',resp.headers['Location'])
|
||||
self.assertEqual('/user/alice.com/following', resp.headers['Location'])
|
||||
self.assertEqual([f'Unfollowed <a href="https://bar/url">bar/url</a>.'],
|
||||
get_flashed_messages())
|
||||
|
||||
|
@ -318,22 +323,27 @@ class UnfollowTest(testutil.TestCase):
|
|||
self.assertEqual(('http://bar/inbox',), inbox_args)
|
||||
self.assert_equals(expected_undo, json_loads(inbox_kwargs['data']))
|
||||
|
||||
follower = Follower.get_by_id('https://bar/id snarfed.org')
|
||||
# check that we signed with the follower's key
|
||||
sig_template = inbox_kwargs['auth'].header_signer.signature_template
|
||||
self.assertTrue(sig_template.startswith('keyId="http://localhost/alice.com"'),
|
||||
sig_template)
|
||||
|
||||
follower = Follower.get_by_id('https://bar/id alice.com')
|
||||
self.assertEqual('inactive', follower.status)
|
||||
|
||||
self.assert_object(
|
||||
'http://localhost/user/snarfed.org/following#undo-2022-01-02T03:04:05-https://bar/id',
|
||||
domains=['snarfed.org'], status='complete',
|
||||
'http://localhost/user/alice.com/following#undo-2022-01-02T03:04:05-https://bar/id',
|
||||
domains=['alice.com'], status='complete',
|
||||
source_protocol='ui', labels=['user', 'activity'],
|
||||
as2=expected_undo, as1=as2.to_as1(expected_undo))
|
||||
|
||||
def test_callback_user_use_instead(self, mock_get, mock_post):
|
||||
user = User.get_or_create('www.snarfed.org')
|
||||
User.get_or_create('snarfed.org', use_instead=user.key)
|
||||
user = User.get_or_create('www.alice.com')
|
||||
User.get_or_create('alice.com', use_instead=user.key)
|
||||
|
||||
self.follower = Follower(
|
||||
id='https://bar/id www.snarfed.org', last_follow=json_dumps(FOLLOW_ADDRESS),
|
||||
src='www.snarfed.org', dest='https://bar/id', status='active',
|
||||
id='https://bar/id www.alice.com', last_follow=json_dumps(FOLLOW_ADDRESS),
|
||||
src='www.alice.com', dest='https://bar/id', status='active',
|
||||
).put()
|
||||
|
||||
mock_get.side_effect = (
|
||||
|
@ -341,26 +351,26 @@ class UnfollowTest(testutil.TestCase):
|
|||
self.as2_resp(FOLLOWEE),
|
||||
)
|
||||
mock_post.side_effect = (
|
||||
requests_response('me=https://snarfed.org'),
|
||||
requests_response('me=https://alice.com'),
|
||||
requests_response('OK'), # AP Undo Follow to inbox
|
||||
)
|
||||
|
||||
state = util.encode_oauth_state({
|
||||
'endpoint': 'http://auth/endpoint',
|
||||
'me': 'https://snarfed.org',
|
||||
'me': 'https://alice.com',
|
||||
'state': self.follower.id(),
|
||||
})
|
||||
with self.client:
|
||||
resp = self.client.get(f'/unfollow/callback?code=my_code&state={state}')
|
||||
self.assertEqual(302, resp.status_code)
|
||||
self.assertEqual('/user/www.snarfed.org/following',resp.headers['Location'])
|
||||
self.assertEqual('/user/www.alice.com/following', resp.headers['Location'])
|
||||
|
||||
id = 'http://localhost/user/www.snarfed.org/following#undo-2022-01-02T03:04:05-https://bar/id'
|
||||
id = 'http://localhost/user/www.alice.com/following#undo-2022-01-02T03:04:05-https://bar/id'
|
||||
expected_undo = {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
'type': 'Undo',
|
||||
'id': id,
|
||||
'actor': 'http://localhost/www.snarfed.org',
|
||||
'actor': 'http://localhost/www.alice.com',
|
||||
'object': FOLLOW_ADDRESS,
|
||||
}
|
||||
|
||||
|
@ -368,9 +378,9 @@ class UnfollowTest(testutil.TestCase):
|
|||
self.assertEqual(('http://bar/inbox',), inbox_args)
|
||||
self.assert_equals(expected_undo, json_loads(inbox_kwargs['data']))
|
||||
|
||||
follower = Follower.get_by_id('https://bar/id www.snarfed.org')
|
||||
follower = Follower.get_by_id('https://bar/id www.alice.com')
|
||||
self.assertEqual('inactive', follower.status)
|
||||
|
||||
self.assert_object(id, domains=['www.snarfed.org'], status='complete',
|
||||
self.assert_object(id, domains=['www.alice.com'], status='complete',
|
||||
source_protocol='ui', labels=['user', 'activity'],
|
||||
as2=expected_undo, as1=as2.to_as1(expected_undo))
|
||||
|
|
|
@ -7,7 +7,7 @@ from urllib.parse import urlencode
|
|||
import feedparser
|
||||
from granary import as2, atom, microformats2
|
||||
from httpsig.sign import HeaderSigner
|
||||
from oauth_dropins.webutil import util
|
||||
from oauth_dropins.webutil import appengine_config, util
|
||||
from oauth_dropins.webutil.appengine_config import tasks_client
|
||||
from oauth_dropins.webutil.appengine_info import APP_ID
|
||||
from oauth_dropins.webutil.testutil import requests_response
|
||||
|
@ -18,7 +18,6 @@ import activitypub
|
|||
from common import (
|
||||
CONNEG_HEADERS_AS2_HTML,
|
||||
CONTENT_TYPE_HTML,
|
||||
default_signature_user,
|
||||
redirect_unwrap,
|
||||
)
|
||||
from models import Follower, Object, Target, User
|
||||
|
@ -108,8 +107,8 @@ REPOST_AS2 = {
|
|||
class WebmentionTest(testutil.TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = User.get_or_create('a')
|
||||
|
||||
self.user_orig = User.get_or_create('orig')
|
||||
self.user_a = User.get_or_create('a')
|
||||
self.orig_html_as2 = requests_response("""\
|
||||
<html>
|
||||
<meta>
|
||||
|
@ -484,7 +483,7 @@ class WebmentionTest(testutil.TestCase):
|
|||
self.assertEqual(as2.CONTENT_TYPE, headers['Content-Type'])
|
||||
|
||||
rsa_key = kwargs['auth'].header_signer._rsa._key
|
||||
self.assertEqual(self.user.private_pem(), rsa_key.exportKey())
|
||||
self.assertEqual(self.user_a.private_pem(), rsa_key.exportKey())
|
||||
|
||||
self.assert_object('http://a/reply',
|
||||
domains=['a'],
|
||||
|
@ -603,13 +602,12 @@ class WebmentionTest(testutil.TestCase):
|
|||
self.assertEqual(as2.CONTENT_TYPE, headers['Content-Type'])
|
||||
|
||||
rsa_key = kwargs['auth'].header_signer._rsa._key
|
||||
self.assertEqual(self.user.private_pem(), rsa_key.exportKey())
|
||||
self.assertEqual(self.user_a.private_pem(), rsa_key.exportKey())
|
||||
|
||||
for args, kwargs in mock_get.call_args_list[1:]:
|
||||
with self.subTest(url=args[0]):
|
||||
rsa_key = kwargs['auth'].header_signer._rsa._key
|
||||
self.assertEqual(default_signature_user().private_pem(),
|
||||
rsa_key.exportKey())
|
||||
self.assertEqual(self.user_a.private_pem(), rsa_key.exportKey())
|
||||
|
||||
self.assert_object('http://a/repost',
|
||||
domains=['a'],
|
||||
|
@ -909,7 +907,7 @@ class WebmentionTest(testutil.TestCase):
|
|||
self.assertEqual(as2.CONTENT_TYPE, headers['Content-Type'])
|
||||
|
||||
rsa_key = kwargs['auth'].header_signer._rsa._key
|
||||
self.assertEqual(self.user.private_pem(), rsa_key.exportKey())
|
||||
self.assertEqual(self.user_a.private_pem(), rsa_key.exportKey())
|
||||
|
||||
self.assert_object('http://a/follow',
|
||||
domains=['a'],
|
||||
|
@ -931,8 +929,8 @@ class WebmentionTest(testutil.TestCase):
|
|||
self.assertEqual(self.follow_as2, json_loads(followers[0].last_follow))
|
||||
|
||||
def test_follow_no_actor(self, mock_get, mock_post):
|
||||
self.user.actor_as2 = json_dumps(self.follow_as2['actor'])
|
||||
self.user.put()
|
||||
self.user_orig.actor_as2 = json_dumps(self.follow_as2['actor'])
|
||||
self.user_orig.put()
|
||||
|
||||
html = self.follow_html.replace(
|
||||
'<a class="p-author h-card" href="https://orig">Ms. ☕ Baz</a>', '')
|
||||
|
@ -976,7 +974,7 @@ class WebmentionTest(testutil.TestCase):
|
|||
self.assert_equals(as2.CONTENT_TYPE, headers['Content-Type'])
|
||||
|
||||
rsa_key = kwargs['auth'].header_signer._rsa._key
|
||||
self.assert_equals(self.user.private_pem(), rsa_key.exportKey())
|
||||
self.assert_equals(self.user_a.private_pem(), rsa_key.exportKey())
|
||||
|
||||
self.assert_object('http://a/follow#2',
|
||||
domains=['a'],
|
||||
|
@ -1033,7 +1031,7 @@ class WebmentionTest(testutil.TestCase):
|
|||
self.assertEqual(as2.CONTENT_TYPE, headers['Content-Type'])
|
||||
|
||||
rsa_key = kwargs['auth'].header_signer._rsa._key
|
||||
self.assertEqual(self.user.private_pem(), rsa_key.exportKey())
|
||||
self.assertEqual(self.user_a.private_pem(), rsa_key.exportKey())
|
||||
|
||||
self.assert_object('http://a/follow',
|
||||
domains=['a'],
|
||||
|
@ -1132,3 +1130,9 @@ class WebmentionTest(testutil.TestCase):
|
|||
object_ids=['https://orig'],
|
||||
labels=['user', 'activity'],
|
||||
)
|
||||
|
||||
def test_no_user(self, mock_get, mock_post):
|
||||
mock_get.side_effect = [requests_response(self.reply_html)]
|
||||
got = self.client.post('/webmention', data={'source': 'https://no-user/post'})
|
||||
self.assertEqual(400, got.status_code)
|
||||
self.assertEqual(0, Object.query().count())
|
||||
|
|
|
@ -55,8 +55,9 @@ class TestCase(unittest.TestCase, testutil.Asserts):
|
|||
**common.CONNEG_HEADERS_AS2_HTML,
|
||||
**kwargs.pop('headers', {}),
|
||||
}
|
||||
return self.req(url, auth=ANY, headers=headers, allow_redirects=False,
|
||||
**kwargs)
|
||||
return self.req(url, data=None, auth=ANY, headers=headers,
|
||||
allow_redirects=False, **kwargs)
|
||||
|
||||
def as2_resp(self, obj):
|
||||
return requests_response(obj, content_type=as2.CONTENT_TYPE)
|
||||
|
||||
|
|
|
@ -116,7 +116,10 @@ class Webmention(View):
|
|||
))
|
||||
logger.info(f'Converted webmention to AS1: {type_label}: {json_dumps(self.source_as1, indent=2)}')
|
||||
|
||||
self.user = User.get_or_create(self.source_domain)
|
||||
self.user = User.get_by_id(self.source_domain)
|
||||
if not self.user:
|
||||
error(f'No user found for domain {self.source_domain}')
|
||||
|
||||
ret = self.try_activitypub()
|
||||
return ret or 'No ActivityPub targets'
|
||||
|
||||
|
@ -210,8 +213,8 @@ class Webmention(View):
|
|||
last_follow=json_dumps(self.source_as2))
|
||||
|
||||
try:
|
||||
last = common.signed_post(inbox, data=self.source_as2,
|
||||
log_data=log_data, user=self.user)
|
||||
last = common.signed_post(inbox, user=self.user, data=self.source_as2,
|
||||
log_data=log_data)
|
||||
obj.delivered.append(target)
|
||||
last_success = last
|
||||
except BaseException as e:
|
||||
|
@ -299,7 +302,7 @@ class Webmention(View):
|
|||
for target in targets:
|
||||
# fetch target page as AS2 object
|
||||
try:
|
||||
self.target_resp = common.get_as2(target)
|
||||
self.target_resp = common.get_as2(target, user=self.user)
|
||||
except (requests.HTTPError, BadGateway) as e:
|
||||
self.target_resp = getattr(e, 'requests_response', None)
|
||||
if self.target_resp and self.target_resp.status_code // 100 == 2:
|
||||
|
@ -327,7 +330,7 @@ class Webmention(View):
|
|||
|
||||
if not inbox_url:
|
||||
# fetch actor as AS object
|
||||
actor = common.get_as2(actor).json()
|
||||
actor = common.get_as2(actor, user=self.user).json()
|
||||
inbox_url = actor.get('inbox')
|
||||
|
||||
if not inbox_url:
|
||||
|
|
Ładowanie…
Reference in New Issue