2019-04-07 20:39:11 +00:00
|
|
|
from django.db import models
|
|
|
|
import logging
|
|
|
|
import json
|
|
|
|
import uuid
|
2019-04-10 19:10:46 +00:00
|
|
|
import re
|
2019-04-07 20:39:11 +00:00
|
|
|
from django.conf import settings
|
|
|
|
from urllib.parse import urlparse
|
|
|
|
from django_kepi import find
|
2019-04-09 19:23:29 +00:00
|
|
|
from httpsig.verify import HeaderVerifier
|
2019-04-07 20:39:11 +00:00
|
|
|
|
2019-04-07 20:51:09 +00:00
|
|
|
logger = logging.getLogger(name='django_kepi')
|
2019-04-07 20:39:11 +00:00
|
|
|
|
|
|
|
# When we receive a message, M, in an inbox, we call validate(M).
|
|
|
|
#
|
|
|
|
# MESSAGE RECEIVED:
|
|
|
|
# Cases:
|
|
|
|
# 1) The claimed sender is a local user (i.e. their host is in ALLOWED_HOSTS).
|
|
|
|
# 2) The claimed sender is remote, and we have their key cached.
|
|
|
|
# 3) The claimed sender is remote, and we know that their account was closed.
|
|
|
|
# (We know this because requesting their details has resulted in
|
|
|
|
# a 410 error in the past.)
|
|
|
|
# 4) The claimed sender is remote, we have no information stored about
|
|
|
|
# their key, and the claimed key obviously belongs to
|
|
|
|
# the user. (This means, at present, that the key is in the
|
|
|
|
# same remote document as the user's profile.)
|
|
|
|
# 5) The claimed sender is remote, we have no information stored about
|
|
|
|
# their key, and the claimed key doesn't obviously belong to
|
|
|
|
# the user.
|
|
|
|
#
|
|
|
|
# Behaviour:
|
|
|
|
# 1) Request the local user's key from the class which is handling Person.
|
|
|
|
# Then go to VALIDATION below.
|
|
|
|
# 2) Go to VALIDATION below.
|
|
|
|
# 3) Drop the message.
|
|
|
|
# 4) Set our "waiting_for" record to the URL we need.
|
|
|
|
# Save our IncomingMessage object.
|
|
|
|
# If there is no existing request for that URL, create a background task
|
|
|
|
# to retrieve its contents. Then go to BACKGROUND TASK FINISHED
|
|
|
|
# below.
|
|
|
|
# 5) Report an error and drop the message.
|
|
|
|
#
|
|
|
|
# VALIDATION:
|
|
|
|
# Cases:
|
|
|
|
# 1) The message passes validation.
|
|
|
|
# 2) The message doesn't pass validation.
|
|
|
|
# Behaviour:
|
|
|
|
# 1) Call handle(M).
|
|
|
|
# 2) Drop the message.
|
|
|
|
#
|
|
|
|
# BACKGROUND TASK FINISHED:
|
|
|
|
# Cases:
|
|
|
|
# 1) We now have a remote user's key.
|
|
|
|
# Cache the key;
|
|
|
|
# For all IncomingMessages which are waiting on that key:
|
|
|
|
# Pass it through to VALIDATION above.
|
|
|
|
# Delete the IncomingMessage.
|
|
|
|
# 2) The remote user doesn't exist (410 Gone, or the host doesn't exist)
|
|
|
|
# Store a blank in the key cache;
|
|
|
|
# Drop the message.
|
|
|
|
# 3) Network issues.
|
|
|
|
# If there's been fewer than "n" tries, recreate the background task.
|
|
|
|
# Otherwise, report the error and drop the message.
|
|
|
|
|
|
|
|
class IncomingMessage(models.Model):
|
|
|
|
|
|
|
|
id = models.UUIDField(
|
|
|
|
primary_key=True,
|
|
|
|
default=uuid.uuid4,
|
|
|
|
editable=False,
|
|
|
|
)
|
|
|
|
|
|
|
|
received_date = models.DateTimeField(auto_now_add=True, blank=True)
|
|
|
|
|
|
|
|
content_type = models.CharField(max_length=255, default='')
|
|
|
|
date = models.CharField(max_length=255, default='')
|
|
|
|
digest = models.CharField(max_length=255, default='')
|
|
|
|
host = models.CharField(max_length=255, default='')
|
2019-04-10 05:35:16 +00:00
|
|
|
path = models.CharField(max_length=255, default='')
|
2019-04-07 20:39:11 +00:00
|
|
|
signature = models.CharField(max_length=255, default='')
|
|
|
|
body = models.TextField(default='')
|
|
|
|
actor = models.CharField(max_length=255, default='')
|
|
|
|
key_id = models.CharField(max_length=255, default='')
|
|
|
|
|
2019-04-10 19:43:37 +00:00
|
|
|
waiting_for = models.URLField(default=None, null=True)
|
2019-04-07 20:39:11 +00:00
|
|
|
|
2019-04-10 19:10:46 +00:00
|
|
|
@property
|
|
|
|
def actor(self):
|
|
|
|
return self.fields['actor']
|
|
|
|
|
|
|
|
@property
|
|
|
|
def key_id(self):
|
|
|
|
return re.findall(r'keyId="([^"]*)"', self.signature)[0]
|
|
|
|
|
2019-04-07 20:39:11 +00:00
|
|
|
def __str__(self):
|
2019-04-07 20:51:09 +00:00
|
|
|
return str(self.id)
|
2019-04-07 20:39:11 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def fields(self):
|
|
|
|
return json.loads(self.body)
|
|
|
|
|
2019-05-05 23:59:19 +00:00
|
|
|
@property
|
|
|
|
def activity_form(self):
|
|
|
|
return self.fields
|
2019-04-28 20:17:10 +00:00
|
|
|
|
2019-04-07 20:39:11 +00:00
|
|
|
def is_local_user(url):
|
|
|
|
return urlparse(url).hostname in settings.ALLOWED_HOSTS
|
|
|
|
|
|
|
|
def _do_validation(message, key):
|
|
|
|
logger.debug('%s: running actual validation', message)
|
|
|
|
fields = message.fields
|
|
|
|
hv = HeaderVerifier(
|
2019-04-10 05:35:16 +00:00
|
|
|
headers = {
|
|
|
|
'Content-Type': message.content_type,
|
|
|
|
'Date': message.date,
|
|
|
|
'Signature': message.signature,
|
|
|
|
},
|
2019-04-07 20:39:11 +00:00
|
|
|
secret = key,
|
|
|
|
method = 'POST',
|
2019-04-10 05:35:16 +00:00
|
|
|
path = message.path,
|
|
|
|
host = message.host,
|
2019-04-07 20:39:11 +00:00
|
|
|
sign_header = 'Signature',
|
|
|
|
)
|
|
|
|
|
|
|
|
if not hv.verify():
|
|
|
|
logger.info('%s: spoofing attempt; message dropped',
|
|
|
|
message)
|
|
|
|
return
|
|
|
|
|
|
|
|
logger.info('%s: validation passed...', message)
|
|
|
|
# XXX okay, go on, do something with it
|
|
|
|
|
|
|
|
def validate(message,
|
|
|
|
second_pass=False):
|
|
|
|
|
|
|
|
actor = message.actor
|
|
|
|
key_id = message.key_id
|
|
|
|
|
|
|
|
logger.debug('%s: begin validation; key_id is %s',
|
|
|
|
message, key_id)
|
2019-04-10 05:35:16 +00:00
|
|
|
logger.debug('%s: message signature is: %s',
|
|
|
|
message, message.signature)
|
2019-04-07 20:39:11 +00:00
|
|
|
logger.debug('%s: message body is: %s',
|
|
|
|
message, message.body)
|
|
|
|
|
|
|
|
if is_local_user(actor):
|
2019-04-10 19:10:46 +00:00
|
|
|
logger.debug('%s: actor %s is local', message, actor)
|
2019-04-07 20:39:11 +00:00
|
|
|
|
|
|
|
local_user = find(actor, 'Actor')
|
|
|
|
|
|
|
|
if local_user is None:
|
|
|
|
logger.info('%s: local actor %s does not exist; dropping message',
|
|
|
|
message, actor)
|
|
|
|
return
|
|
|
|
|
|
|
|
key = local_user.key
|
|
|
|
_do_validation(message, key)
|
|
|
|
return
|
|
|
|
|
2019-04-10 16:41:46 +00:00
|
|
|
if not _obviously_belongs_to(actor, key_id):
|
|
|
|
logger.info('%s: key_id %s is not obviously owned by '+\
|
|
|
|
'actor %s; dropping message',
|
|
|
|
message, key_id, actor)
|
|
|
|
return
|
|
|
|
|
2019-04-07 20:39:11 +00:00
|
|
|
try:
|
2019-04-18 18:32:55 +00:00
|
|
|
remote_key = CachedRemoteUser.objects.get(owner=actor)
|
|
|
|
except CachedRemoteUser.DoesNotExist:
|
2019-04-07 20:39:11 +00:00
|
|
|
remote_key = None
|
|
|
|
|
|
|
|
if remote_key is not None:
|
|
|
|
|
|
|
|
if remote_key.is_gone():
|
|
|
|
# XXX This should probably trigger a clean-out of everything
|
|
|
|
# we know about that user
|
|
|
|
logger.info('%s: remote actor %s is gone; dropping message',
|
|
|
|
actor, message)
|
|
|
|
return
|
|
|
|
|
|
|
|
logger.debug('%s: we have the remote key', message)
|
2019-04-10 16:41:46 +00:00
|
|
|
_do_validation(message, remote_key.key)
|
2019-04-07 20:39:11 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
logger.debug('%s: we don\'t have the key', message)
|
|
|
|
|
|
|
|
if second_pass:
|
|
|
|
logger.warning('%s: we apparently both do and don\'t have the key',
|
|
|
|
message)
|
|
|
|
return
|
|
|
|
|
|
|
|
message.waiting_for = actor
|
|
|
|
message.save()
|
|
|
|
|
|
|
|
if len(IncomingMessage.objects.filter(waiting_for=actor))==1:
|
|
|
|
logger.debug('%s: starting background task', message)
|
|
|
|
_kick_off_background_fetch(actor)
|
|
|
|
else:
|
|
|
|
logger.debug('%s: not starting background task', message)
|
|
|
|
|