2019-06-27 16:19:02 +00:00
|
|
|
from django_kepi.create import create
|
2019-05-27 20:18:28 +00:00
|
|
|
from django_kepi.validation import IncomingMessage, validate
|
2019-05-29 00:12:08 +00:00
|
|
|
from django_kepi.models.actor import Actor
|
2019-07-20 22:26:46 +00:00
|
|
|
from django.conf import settings
|
2019-05-29 08:32:20 +00:00
|
|
|
import django.test
|
2019-05-22 17:13:50 +00:00
|
|
|
import httpretty
|
|
|
|
import logging
|
2019-05-27 20:18:28 +00:00
|
|
|
import httpsig
|
|
|
|
import json
|
|
|
|
|
|
|
|
ACTIVITY_ID = "https://example.com/04b065f8-81c4-408e-bec3-9fb1f7c06408"
|
2019-05-29 09:31:12 +00:00
|
|
|
ACTIVITY_DATE = "Thu, 04 Apr 2019 21:12:11 GMT"
|
2019-05-27 20:18:28 +00:00
|
|
|
INBOX_HOST = 'europa.example.com'
|
2019-05-29 09:31:12 +00:00
|
|
|
INBOX_PATH = '/sharedInbox'
|
2019-05-27 20:18:28 +00:00
|
|
|
|
|
|
|
REMOTE_FRED = 'https://remote.example.org/users/fred'
|
|
|
|
REMOTE_JIM = 'https://remote.example.org/users/jim'
|
|
|
|
|
|
|
|
FREDS_INBOX = REMOTE_FRED+'/inbox'
|
|
|
|
JIMS_INBOX = REMOTE_JIM+'/inbox'
|
|
|
|
REMOTE_SHARED_INBOX = 'https://remote.example.org/shared-inbox'
|
|
|
|
|
2019-08-13 18:23:43 +00:00
|
|
|
LOCAL_ALICE = 'https://testserver/users/alice'
|
|
|
|
LOCAL_BOB = 'https://testserver/users/bob'
|
2019-05-27 20:18:28 +00:00
|
|
|
|
2019-06-14 20:09:43 +00:00
|
|
|
FREDS_FOLLOWERS = REMOTE_FRED+'/followers'
|
|
|
|
JIMS_FOLLOWERS = REMOTE_JIM+'/followers'
|
|
|
|
ALICES_FOLLOWERS = LOCAL_ALICE+'/followers'
|
|
|
|
BOBS_FOLLOWERS = LOCAL_BOB+'/followers'
|
|
|
|
|
2019-06-13 18:38:26 +00:00
|
|
|
PUBLIC = "https://www.w3.org/ns/activitystreams#Public"
|
|
|
|
|
2019-06-14 20:09:43 +00:00
|
|
|
CONTEXT_URL = "https://www.w3.org/ns/activitystreams"
|
2019-05-27 20:18:28 +00:00
|
|
|
|
2019-05-22 17:13:50 +00:00
|
|
|
logger = logging.getLogger(name='django_kepi')
|
2019-05-22 16:42:07 +00:00
|
|
|
|
2019-06-27 15:16:38 +00:00
|
|
|
def create_local_person(name='jemima',
|
2019-08-10 16:01:08 +00:00
|
|
|
load_default_keys_from='tests/keys/keys-0003.json',
|
2019-05-22 17:06:20 +00:00
|
|
|
**kwargs):
|
2019-08-10 16:01:08 +00:00
|
|
|
|
|
|
|
if 'publicKey' or 'privateKey' not in kwargs:
|
|
|
|
keys = json.load(open(load_default_keys_from, 'r'))
|
|
|
|
|
|
|
|
if 'publicKey' not in kwargs:
|
|
|
|
kwargs['publicKey'] = keys['public']
|
|
|
|
|
|
|
|
if 'privateKey' not in kwargs:
|
|
|
|
kwargs['privateKey'] = keys['private']
|
|
|
|
|
|
|
|
else:
|
|
|
|
keys = None
|
|
|
|
|
2019-05-22 17:06:20 +00:00
|
|
|
spec = {
|
2019-05-22 16:42:07 +00:00
|
|
|
'name': name,
|
2019-07-19 21:12:56 +00:00
|
|
|
'preferredUsername': name,
|
2019-08-10 15:22:36 +00:00
|
|
|
'id': settings.KEPI['USER_URL_FORMAT'] % {
|
|
|
|
'username': name,
|
|
|
|
'hostname': settings.KEPI['LOCAL_OBJECT_HOSTNAME'],
|
|
|
|
},
|
2019-05-22 16:42:07 +00:00
|
|
|
'type': 'Person',
|
2019-08-10 15:22:36 +00:00
|
|
|
'endpoints': {
|
|
|
|
'sharedInbox': settings.KEPI['SHARED_INBOX'] % {
|
|
|
|
'hostname': settings.KEPI['LOCAL_OBJECT_HOSTNAME'],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
'inbox': settings.KEPI['SHARED_INBOX'] % {
|
|
|
|
'hostname': settings.KEPI['LOCAL_OBJECT_HOSTNAME'],
|
|
|
|
},
|
2019-05-22 17:06:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
spec.update(kwargs)
|
|
|
|
|
2019-07-30 18:18:34 +00:00
|
|
|
if 'publicKey' in spec:
|
|
|
|
spec['publicKey'] = {
|
|
|
|
'id': spec['id']+'#main-key',
|
|
|
|
'owner': spec['id'],
|
|
|
|
'publicKeyPem': spec['publicKey'],
|
|
|
|
}
|
|
|
|
|
2019-07-31 18:50:41 +00:00
|
|
|
result = create(
|
|
|
|
value = spec,
|
|
|
|
run_delivery = False,
|
|
|
|
)
|
2019-05-29 00:12:08 +00:00
|
|
|
|
|
|
|
return result
|
2019-05-22 16:42:07 +00:00
|
|
|
|
2019-06-27 15:16:38 +00:00
|
|
|
def create_local_note(**kwargs):
|
|
|
|
spec = {
|
2019-08-13 18:23:43 +00:00
|
|
|
'id': 'https://testserver/testing-note',
|
2019-06-27 15:16:38 +00:00
|
|
|
'type': 'Note',
|
|
|
|
'content': 'This is just a test.',
|
|
|
|
}
|
|
|
|
|
|
|
|
spec.update(kwargs)
|
|
|
|
|
|
|
|
result = create(**spec)
|
|
|
|
return result
|
|
|
|
|
2019-05-27 20:18:28 +00:00
|
|
|
def mock_remote_object(
|
2019-05-22 17:13:50 +00:00
|
|
|
url,
|
|
|
|
ftype = 'Object',
|
|
|
|
content = '',
|
|
|
|
status = 200,
|
2019-06-03 22:03:09 +00:00
|
|
|
as_post = False,
|
2019-08-12 18:42:23 +00:00
|
|
|
on_fetch = None,
|
2019-05-22 17:13:50 +00:00
|
|
|
):
|
|
|
|
|
|
|
|
headers = {
|
|
|
|
'Content-Type': 'application/activity+json',
|
|
|
|
}
|
|
|
|
|
2019-05-29 10:18:10 +00:00
|
|
|
if isinstance(content, bytes):
|
|
|
|
body = content
|
|
|
|
else:
|
|
|
|
body = bytes(content, encoding='UTF-8')
|
|
|
|
|
2019-06-03 22:03:09 +00:00
|
|
|
if as_post:
|
|
|
|
method = httpretty.POST
|
|
|
|
else:
|
|
|
|
method = httpretty.GET
|
|
|
|
|
2019-08-12 18:42:23 +00:00
|
|
|
def return_body(request, url, stuff):
|
|
|
|
logger.info('%s: fetched', url)
|
|
|
|
if on_fetch is not None:
|
|
|
|
on_fetch()
|
|
|
|
return status, stuff, body
|
|
|
|
|
2019-05-22 17:13:50 +00:00
|
|
|
httpretty.register_uri(
|
2019-06-03 22:03:09 +00:00
|
|
|
method,
|
2019-05-22 17:13:50 +00:00
|
|
|
url,
|
|
|
|
status=status,
|
|
|
|
headers=headers,
|
2019-08-12 18:42:23 +00:00
|
|
|
body = return_body,
|
2019-06-14 23:41:05 +00:00
|
|
|
match_querystring = True,
|
2019-05-29 10:18:10 +00:00
|
|
|
)
|
2019-05-22 17:13:50 +00:00
|
|
|
|
|
|
|
logger.debug('Mocking %s as %d: %s',
|
|
|
|
url,
|
|
|
|
status,
|
|
|
|
content)
|
2019-05-27 20:18:28 +00:00
|
|
|
|
2019-05-29 08:52:52 +00:00
|
|
|
def create_remote_person(
|
|
|
|
url,
|
|
|
|
name,
|
2019-05-29 09:46:15 +00:00
|
|
|
publicKey,
|
2019-08-12 18:42:23 +00:00
|
|
|
on_fetch = None,
|
2019-05-29 09:46:15 +00:00
|
|
|
**fields):
|
2019-05-29 08:52:52 +00:00
|
|
|
|
2019-08-12 18:42:23 +00:00
|
|
|
body = json.dumps(
|
|
|
|
remote_user(
|
2019-05-29 08:52:52 +00:00
|
|
|
url=url,
|
|
|
|
name=name,
|
|
|
|
publicKey = publicKey,
|
2019-05-29 09:46:15 +00:00
|
|
|
**fields,
|
2019-08-12 18:42:23 +00:00
|
|
|
))
|
|
|
|
|
|
|
|
mock_remote_object(
|
|
|
|
url=url,
|
|
|
|
on_fetch=on_fetch,
|
|
|
|
content=body,
|
2019-05-29 08:52:52 +00:00
|
|
|
)
|
|
|
|
|
2019-06-14 20:09:43 +00:00
|
|
|
def create_remote_collection(
|
|
|
|
url,
|
|
|
|
items,
|
|
|
|
number_per_page = 10,
|
|
|
|
):
|
|
|
|
|
|
|
|
PAGE_URL_FORMAT = '%s?page=%d'
|
|
|
|
|
|
|
|
mock_remote_object(
|
|
|
|
url=url,
|
|
|
|
content=json.dumps({
|
|
|
|
"@context" : "https://www.w3.org/ns/activitystreams",
|
|
|
|
"id" : url,
|
|
|
|
"type" : "OrderedCollection",
|
|
|
|
"totalItems" : len(items),
|
|
|
|
"first" : PAGE_URL_FORMAT % (url, 1),
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
|
|
|
|
page_count = len(items)//number_per_page
|
|
|
|
for i in range(1, page_count+2):
|
|
|
|
|
|
|
|
fields = {
|
|
|
|
"@context" : CONTEXT_URL,
|
|
|
|
"id" : PAGE_URL_FORMAT % (url, i),
|
|
|
|
"type" : "OrderedCollectionPage",
|
|
|
|
"totalItems" : len(items),
|
|
|
|
"partOf": url,
|
|
|
|
"orderedItems": items[(i-1)*number_per_page:i*number_per_page],
|
|
|
|
}
|
|
|
|
|
|
|
|
if i>1:
|
|
|
|
fields['prev'] = PAGE_URL_FORMAT % (url, i-1)
|
|
|
|
|
|
|
|
if i<page_count+1:
|
|
|
|
fields['next'] = PAGE_URL_FORMAT % (url, i+1)
|
|
|
|
|
|
|
|
mock_remote_object(
|
|
|
|
url = PAGE_URL_FORMAT % (url, i),
|
|
|
|
content=json.dumps(fields),
|
|
|
|
)
|
|
|
|
|
2019-05-29 08:15:39 +00:00
|
|
|
def test_message_body_and_headers(secret='',
|
|
|
|
path=INBOX_PATH,
|
|
|
|
host=INBOX_HOST,
|
2019-07-19 21:17:47 +00:00
|
|
|
signed = True,
|
2019-05-29 08:15:39 +00:00
|
|
|
**fields):
|
2019-05-27 20:18:28 +00:00
|
|
|
|
|
|
|
body = dict([(f[2:],v) for f,v in fields.items() if f.startswith('f_')])
|
2019-08-12 18:42:23 +00:00
|
|
|
body['@context'] = CONTEXT_URL
|
2019-05-29 09:31:12 +00:00
|
|
|
body['Host'] = host
|
2019-05-27 20:18:28 +00:00
|
|
|
|
|
|
|
headers = {
|
|
|
|
'content-type': "application/activity+json",
|
2019-05-29 09:31:12 +00:00
|
|
|
'date': ACTIVITY_DATE,
|
2019-05-29 08:15:39 +00:00
|
|
|
'host': host,
|
2019-05-27 20:18:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if 'key_id' in fields:
|
|
|
|
key_id = fields['key_id']
|
|
|
|
else:
|
|
|
|
key_id = body['actor']+'#main-key'
|
|
|
|
|
2019-08-12 22:49:16 +00:00
|
|
|
if signed:
|
2019-05-27 20:18:28 +00:00
|
|
|
|
2019-08-12 22:49:16 +00:00
|
|
|
signer = httpsig.HeaderSigner(
|
|
|
|
secret=secret,
|
|
|
|
algorithm='rsa-sha256',
|
|
|
|
key_id = key_id,
|
|
|
|
headers=['(request-target)', 'host', 'date', 'content-type'],
|
|
|
|
)
|
|
|
|
|
|
|
|
headers = signer.sign(
|
|
|
|
headers,
|
|
|
|
method='POST',
|
|
|
|
path=path,
|
|
|
|
)
|
2019-05-27 20:18:28 +00:00
|
|
|
|
2019-08-12 22:49:16 +00:00
|
|
|
SIGNATURE = 'Signature'
|
|
|
|
if headers['Authorization'].startswith(SIGNATURE):
|
|
|
|
headers['Signature'] = headers['Authorization'][len(SIGNATURE)+1:]
|
2019-05-27 20:18:28 +00:00
|
|
|
|
2019-08-01 16:54:31 +00:00
|
|
|
if 'id' not in body:
|
|
|
|
body['id'] = ACTIVITY_ID
|
|
|
|
|
2019-08-12 18:42:23 +00:00
|
|
|
body = json.dumps(body, indent=2, sort_keys=True)
|
|
|
|
|
2019-05-29 08:15:39 +00:00
|
|
|
return body, headers
|
|
|
|
|
|
|
|
def test_message(secret='', **fields):
|
|
|
|
|
|
|
|
body, headers = test_message_body_and_headers(
|
|
|
|
secret,
|
|
|
|
**fields,
|
|
|
|
)
|
|
|
|
|
2019-05-27 20:18:28 +00:00
|
|
|
result = IncomingMessage(
|
|
|
|
content_type = headers['content-type'],
|
|
|
|
date = headers['date'],
|
|
|
|
digest = '', # FIXME ???
|
|
|
|
host = headers['host'],
|
|
|
|
path = INBOX_PATH,
|
|
|
|
signature = headers['Signature'],
|
|
|
|
body = json.dumps(body, sort_keys=True),
|
|
|
|
)
|
|
|
|
|
|
|
|
result.save()
|
|
|
|
return result
|
|
|
|
|
2019-05-29 08:32:20 +00:00
|
|
|
def post_test_message(
|
|
|
|
secret,
|
2019-05-29 08:45:28 +00:00
|
|
|
path=INBOX_PATH,
|
|
|
|
host=INBOX_HOST,
|
|
|
|
f_id=ACTIVITY_ID,
|
2019-05-29 08:32:20 +00:00
|
|
|
client = None,
|
2019-08-05 12:30:34 +00:00
|
|
|
content = None,
|
2019-08-01 16:54:31 +00:00
|
|
|
**fields,
|
2019-05-29 08:32:20 +00:00
|
|
|
):
|
|
|
|
|
|
|
|
if client is None:
|
|
|
|
client = django.test.Client()
|
|
|
|
|
|
|
|
body, headers = test_message_body_and_headers(
|
|
|
|
secret = secret,
|
|
|
|
path = path,
|
|
|
|
host = host,
|
2019-08-01 16:54:31 +00:00
|
|
|
**fields,
|
2019-05-29 08:32:20 +00:00
|
|
|
)
|
|
|
|
|
2019-08-05 12:30:34 +00:00
|
|
|
if content is None:
|
2019-08-12 22:49:16 +00:00
|
|
|
content = body
|
2019-08-05 12:30:34 +00:00
|
|
|
|
2019-05-29 08:32:20 +00:00
|
|
|
logger.debug("Test message is %s",
|
|
|
|
body)
|
|
|
|
logger.debug(" -- with headers %s",
|
|
|
|
headers)
|
|
|
|
|
2019-08-01 16:54:31 +00:00
|
|
|
response = client.post(
|
2019-05-29 08:32:20 +00:00
|
|
|
path = path,
|
2019-05-29 08:33:51 +00:00
|
|
|
content_type = headers['content-type'],
|
2019-08-05 12:30:34 +00:00
|
|
|
data = content,
|
2019-05-29 08:32:20 +00:00
|
|
|
HTTP_DATE = headers['date'],
|
2019-08-01 16:54:31 +00:00
|
|
|
HTTP_HOST = headers['host'],
|
2019-05-29 08:32:20 +00:00
|
|
|
HTTP_SIGNATURE = headers['signature'],
|
|
|
|
)
|
|
|
|
|
2019-08-01 16:54:31 +00:00
|
|
|
return response
|
2019-05-29 08:32:20 +00:00
|
|
|
|
2019-05-27 20:18:28 +00:00
|
|
|
def remote_user(url, name,
|
|
|
|
publicKey='',
|
|
|
|
inbox=None,
|
|
|
|
sharedInbox=None,
|
2019-06-14 20:18:22 +00:00
|
|
|
followers=None,
|
2019-05-27 20:18:28 +00:00
|
|
|
):
|
|
|
|
result = {
|
2019-08-12 18:42:23 +00:00
|
|
|
'@context': CONTEXT_URL,
|
2019-05-27 20:18:28 +00:00
|
|
|
'id': url,
|
|
|
|
'type': 'Person',
|
|
|
|
'following': '',
|
2019-06-14 20:18:22 +00:00
|
|
|
'followers': followers,
|
2019-05-27 20:18:28 +00:00
|
|
|
'outbox': '',
|
|
|
|
'featured': '',
|
|
|
|
'preferredUsername': name,
|
|
|
|
'url': url,
|
|
|
|
'publicKey': {
|
|
|
|
'id': url+'#main-key',
|
|
|
|
'owner': url,
|
|
|
|
'publicKeyPem': publicKey,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
if inbox is not None:
|
|
|
|
result['inbox'] = inbox
|
|
|
|
|
|
|
|
if sharedInbox is not None:
|
|
|
|
result['endpoints'] = {
|
|
|
|
'sharedInbox': sharedInbox,
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
2019-08-12 18:42:23 +00:00
|
|
|
|
|
|
|
def remote_object_is_recorded(url):
|
|
|
|
|
|
|
|
from django_kepi.models import Object
|
|
|
|
|
|
|
|
try:
|
|
|
|
result = Object.objects.get(remote_url=url)
|
|
|
|
return True
|
|
|
|
except Object.DoesNotExist:
|
|
|
|
return False
|