2017-08-20 14:28:40 +00:00
|
|
|
"""Handles requests for Salmon endpoints: actors, inbox, etc.
|
2017-08-20 18:35:14 +00:00
|
|
|
|
|
|
|
https://github.com/salmon-protocol/salmon-protocol/blob/master/draft-panzer-salmon-00.html
|
|
|
|
https://github.com/salmon-protocol/salmon-protocol/blob/master/draft-panzer-magicsig-01.html
|
2017-08-20 14:28:40 +00:00
|
|
|
"""
|
|
|
|
import json
|
|
|
|
import logging
|
2017-08-20 18:33:00 +00:00
|
|
|
from xml.etree import ElementTree
|
2017-08-20 14:28:40 +00:00
|
|
|
|
|
|
|
import appengine_config
|
|
|
|
|
2017-08-20 18:33:00 +00:00
|
|
|
from django_salmon import magicsigs, utils
|
2017-08-20 14:28:40 +00:00
|
|
|
import webapp2
|
|
|
|
from webmentiontools import send
|
|
|
|
|
|
|
|
import common
|
2017-10-10 00:29:50 +00:00
|
|
|
from models import Response
|
2017-08-20 14:28:40 +00:00
|
|
|
|
2017-08-20 18:33:00 +00:00
|
|
|
# from django_salmon.feeds
|
|
|
|
ATOM_NS = 'http://www.w3.org/2005/Atom'
|
|
|
|
ATOM_THREADING_NS = 'http://purl.org/syndication/thread/1.0'
|
|
|
|
|
2017-08-20 14:28:40 +00:00
|
|
|
|
|
|
|
class SlapHandler(webapp2.RequestHandler):
|
2017-09-03 19:54:10 +00:00
|
|
|
"""Accepts POSTs to /[ACCT]/salmon and converts to outbound webmentions."""
|
2017-08-20 14:28:40 +00:00
|
|
|
|
|
|
|
# TODO: unify with activitypub
|
2017-09-03 19:54:10 +00:00
|
|
|
def post(self, username, domain):
|
2017-08-20 14:28:40 +00:00
|
|
|
logging.info('Got: %s', self.request.body)
|
|
|
|
|
2017-08-20 18:33:00 +00:00
|
|
|
parsed = utils.parse_magic_envelope(self.request.body)
|
|
|
|
data = utils.decode(parsed['data'])
|
|
|
|
logging.info('Decoded: %s', data)
|
|
|
|
|
|
|
|
# verify signature
|
|
|
|
author = utils.parse_author_uri_from_atom(data)
|
|
|
|
if ':' not in author:
|
|
|
|
author = 'acct:%s' % author
|
|
|
|
elif not author.startswith('acct:'):
|
2017-08-23 15:14:51 +00:00
|
|
|
common.error(self, 'Author URI %s has unsupported scheme; expected acct:' % author)
|
2017-08-20 18:33:00 +00:00
|
|
|
|
|
|
|
logging.info('Fetching Salmon key for %s' % author)
|
2017-09-03 19:54:10 +00:00
|
|
|
if not magicsigs.verify(author, data, parsed['sig']):
|
2017-08-23 15:14:51 +00:00
|
|
|
common.error(self, 'Could not verify magic signature.')
|
2017-08-20 18:33:00 +00:00
|
|
|
logging.info('Verified magic signature.')
|
|
|
|
|
2017-09-03 19:54:10 +00:00
|
|
|
# Verify that the timestamp is recent. Required by spec.
|
|
|
|
# I get that this helps prevent spam, but in practice it's a bit silly,
|
|
|
|
# and other major implementations don't (e.g. Mastodon), so forget it.
|
|
|
|
#
|
2017-09-02 03:49:00 +00:00
|
|
|
# updated = utils.parse_updated_from_atom(data)
|
|
|
|
# if not utils.verify_timestamp(updated):
|
|
|
|
# common.error(self, 'Timestamp is more than 1h old.')
|
2017-08-20 18:33:00 +00:00
|
|
|
|
|
|
|
# find webmention source and target
|
|
|
|
source = None
|
|
|
|
targets = []
|
|
|
|
for elem in ElementTree.fromstring(data):
|
|
|
|
if elem.tag == utils.normalize('link', ATOM_NS):
|
|
|
|
source = elem.attrib.get('href').strip()
|
|
|
|
elif elem.tag == utils.normalize('in-reply-to', ATOM_THREADING_NS):
|
|
|
|
target = elem.attrib.get('ref') or elem.text
|
|
|
|
if target and target not in targets:
|
|
|
|
targets.append(target.strip())
|
|
|
|
|
|
|
|
if not source:
|
2017-08-23 15:14:51 +00:00
|
|
|
common.error(self, "Couldn't find post URL (link element)")
|
2017-08-20 14:28:40 +00:00
|
|
|
if not targets:
|
2017-08-20 18:33:00 +00:00
|
|
|
self.error("Couldn't find target URL (thr:in-reply-to or TODO)")
|
2017-08-20 14:28:40 +00:00
|
|
|
|
2017-08-20 18:33:00 +00:00
|
|
|
# send webmentions!
|
2017-08-20 14:28:40 +00:00
|
|
|
errors = []
|
|
|
|
for target in targets:
|
2017-10-10 00:29:50 +00:00
|
|
|
response = Response.get_or_insert(
|
2017-10-10 02:11:40 +00:00
|
|
|
'%s %s' % (source, target), direction='in', protocol='ostatus',
|
|
|
|
source_atom=data)
|
2017-08-20 14:28:40 +00:00
|
|
|
logging.info('Sending webmention from %s to %s', source, target)
|
|
|
|
wm = send.WebmentionSend(source, target)
|
|
|
|
if wm.send(headers=common.HEADERS):
|
|
|
|
logging.info('Success: %s', wm.response)
|
2017-10-10 00:29:50 +00:00
|
|
|
response.status = 'complete'
|
2017-08-20 14:28:40 +00:00
|
|
|
else:
|
|
|
|
logging.warning('Failed: %s', wm.error)
|
|
|
|
errors.append(wm.error)
|
2017-10-10 00:29:50 +00:00
|
|
|
response.status = 'error'
|
|
|
|
response.put()
|
2017-08-20 14:28:40 +00:00
|
|
|
|
|
|
|
if errors:
|
|
|
|
self.abort(errors[0].get('http_status') or 400,
|
|
|
|
'Errors:\n' + '\n'.join(json.dumps(e, indent=2) for e in errors))
|
|
|
|
|
|
|
|
|
|
|
|
app = webapp2.WSGIApplication([
|
2017-09-03 19:54:10 +00:00
|
|
|
(r'/%s/salmon' % common.ACCT_RE, SlapHandler),
|
2017-08-20 14:28:40 +00:00
|
|
|
], debug=appengine_config.DEBUG)
|