def slap(content, source, user, mime_type='application/atom+xml'): """Notify a source of updated content.""" from django_salmon.models import Subscription, UserKeyPair sub = Subscription.objects.get_for_object(source) if not sub: return # nothing to do keypair = UserKeyPair.objects.get_or_create(user) magic_envelope = magicsigs.magic_envelope( content, mime_type, keypair) # TODO(paulosman) # really crappy HTTP client right now. Can improve when the basic # protocol flow is working. headers = { 'Content-Type': 'application/magic-envelope+xml', } req = urllib2.Request(sub.salmon_endpoint, magic_envelope, headers) try: response = urllib2.urlopen(req) print response.read() except urllib2.HTTPError, e: print repr(e) print e.read()
def test_bad_inner_xml(self, *mocks): slap = magicsigs.magic_envelope('not xml', common.CONTENT_TYPE_ATOM, self.key) got = application.get_response('/foo.com/salmon', method='POST', body=slap) self.assertEqual(400, got.status_int)
def test_rsvp_not_supported(self, *mocks): slap = magicsigs.magic_envelope("""\ <?xml version='1.0' encoding='UTF-8'?> <entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'> <uri>https://my/rsvp</uri> <activity:verb>http://activitystrea.ms/schema/1.0/rsvp</activity:verb> <activity:object>http://orig/event</activity:object> </entry>""", common.CONTENT_TYPE_ATOM, self.key) got = self.client.post('/foo.com/salmon', data=slap) self.assertEqual(501, got.status_code)
def send_slap(self, mock_urlopen, mock_head, mock_get, mock_post, atom_slap): # salmon magic key discovery. first host-meta, then webfinger mock_urlopen.side_effect = [ UrlopenResult( 200, """\ <?xml version='1.0' encoding='UTF-8'?> <XRD xmlns='http://docs.oasis-open.org/ns/xri/xrd-1.0'> <Link rel='lrdd' type='application/xrd+xml' template='http://webfinger/{uri}' /> </XRD>"""), UrlopenResult( 200, """\ <?xml version='1.0' encoding='UTF-8'?> <XRD xmlns='http://docs.oasis-open.org/ns/xri/xrd-1.0'> <Subject>[email protected]</Subject> <Link rel='magic-public-key' href='%s' /> </XRD>""" % self.key.href()), ] # webmention discovery mock_head.return_value = requests_response(url='http://orig/post') mock_get.return_value = requests_response( '<html><head><link rel="webmention" href="/webmention"></html>') # webmention post mock_post.return_value = requests_response() slap = magicsigs.magic_envelope(atom_slap, common.CONTENT_TYPE_ATOM, self.key) got = application.get_response('/[email protected]/salmon', method='POST', body=slap) self.assertEqual(200, got.status_int) # check salmon magic key discovery mock_urlopen.assert_has_calls(( mock.call('http://fedsoc.net/.well-known/host-meta'), mock.call('http://webfinger/[email protected]'), )) # check webmention discovery self.expected_headers = copy.deepcopy(common.HEADERS) self.expected_headers['Accept'] = '*/*' mock_get.assert_called_once_with('http://orig/post', headers=common.HEADERS, verify=False)
def envelope(self): """Signs and encloses this salmon in a Magic Signature envelope. Fetches the author's Magic Signatures public key via LRDD in order to create the signature. Returns: JSON dict following Magic Signatures spec section 3.5: http://salmon-protocol.googlecode.com/svn/trunk/draft-panzer-magicsig-01.html#anchor5 """ class UserKey(object): def __init__(self, **kwargs): vars(self).update(**kwargs) salmon_vars = json.loads(self.vars) key_url = USER_KEY_HANDLER % (salmon_vars["author_uri"], appengine_config.USER_KEY_HANDLER_SECRET) key = UserKey(**json.loads(util.urlfetch(key_url))) return XML_DOCTYPE_LINE + magicsigs.magic_envelope( ATOM_SALMON_TEMPLATE % salmon_vars, "application/atom+xml", key )
def _try_salmon(self, resp): """ Args: resp: Response """ # fetch target HTML page, extract Atom rel-alternate link target = resp.target() if not self.target_resp: self.target_resp = common.requests_get(target) parsed = util.parse_html(self.target_resp) atom_url = parsed.find('link', rel='alternate', type=common.CONTENT_TYPE_ATOM) if not atom_url or not atom_url.get('href'): self.error('Target post %s has no Atom link' % resp.target(), status=400) # fetch Atom target post, extract and inject id into source object base_url = '' base = parsed.find('base') if base and base.get('href'): base_url = base['href'] atom_link = parsed.find('link', rel='alternate', type=common.CONTENT_TYPE_ATOM) atom_url = urllib.parse.urljoin( resp.target(), urllib.parse.urljoin(base_url, atom_link['href'])) feed = common.requests_get(atom_url).text parsed = feedparser.parse(feed) logging.info('Parsed: %s', json_dumps(parsed, indent=2)) entry = parsed.entries[0] target_id = entry.id in_reply_to = self.source_obj.get('inReplyTo') source_obj_obj = self.source_obj.get('object') if in_reply_to: for elem in in_reply_to: if elem.get('url') == target: elem['id'] = target_id elif isinstance(source_obj_obj, dict): source_obj_obj['id'] = target_id # Mastodon (and maybe others?) require a rel-mentioned link to the # original post's author to make it show up as a reply: # app/services/process_interaction_service.rb # ...so add them as a tag, which atom renders as a rel-mention link. authors = entry.get('authors', None) if authors: url = entry.authors[0].get('href') if url: self.source_obj.setdefault('tags', []).append({'url': url}) # extract and discover salmon endpoint logging.info('Discovering Salmon endpoint in %s', atom_url) endpoint = django_salmon.discover_salmon_endpoint(feed) if not endpoint: # try webfinger parsed = urllib.parse.urlparse(resp.target()) # TODO: test missing email author = entry.get('author_detail', {}) email = author.get('email') or '@'.join( (author.get('name', ''), parsed.netloc)) try: # TODO: always https? profile = common.requests_get( '%s://%s/.well-known/webfinger?resource=acct:%s' % (parsed.scheme, parsed.netloc, email), verify=False) endpoint = django_salmon.get_salmon_replies_link( profile.json()) except requests.HTTPError as e: pass if not endpoint: self.error('No salmon endpoint found!', status=400) logging.info('Discovered Salmon endpoint %s', endpoint) # construct reply Atom object self.source_url = resp.source() activity = self.source_obj if self.source_obj.get('verb') not in source.VERBS_WITH_OBJECT: activity = {'object': self.source_obj} entry = atom.activity_to_atom(activity, xml_base=self.source_url) logging.info('Converted %s to Atom:\n%s', self.source_url, entry) # sign reply and wrap in magic envelope domain = urllib.parse.urlparse(self.source_url).netloc key = MagicKey.get_or_create(domain) logging.info('Using key for %s: %s', domain, key) magic_envelope = magicsigs.magic_envelope(entry, common.CONTENT_TYPE_ATOM, key).decode() logging.info('Sending Salmon slap to %s', endpoint) common.requests_post( endpoint, data=common.XML_UTF8 + magic_envelope, headers={'Content-Type': common.CONTENT_TYPE_MAGIC_ENVELOPE}) return True
def send_salmon(self, source_obj, target_resp=None): self.resp.protocol = 'ostatus' # fetch target HTML page, extract Atom rel-alternate link if not target_resp: target_resp = common.requests_get(self.resp.target()) parsed = BeautifulSoup(target_resp.content, from_encoding=target_resp.encoding) atom_url = parsed.find('link', rel='alternate', type=common.CONTENT_TYPE_ATOM) if not atom_url or not atom_url.get('href'): common.error(self, 'Target post %s has no Atom link' % self.resp.target(), status=400) # fetch Atom target post, extract and inject id into source object feed = common.requests_get(atom_url['href']).text parsed = feedparser.parse(feed) logging.info('Parsed: %s', json.dumps(parsed, indent=2, default=lambda key: '-')) entry = parsed.entries[0] target_id = entry.id in_reply_to = source_obj.get('inReplyTo') source_obj_obj = source_obj.get('object') if in_reply_to: in_reply_to[0]['id'] = target_id elif isinstance(source_obj_obj, dict): source_obj_obj['id'] = target_id # Mastodon (and maybe others?) require a rel-mentioned link to the # original post's author to make it show up as a reply: # app/services/process_interaction_service.rb # ...so add them as a tag, which atom renders as a rel-mention link. authors = entry.get('authors', None) if authors: url = entry.authors[0].get('href') if url: source_obj.setdefault('tags', []).append({'url': url}) # extract and discover salmon endpoint logging.info('Discovering Salmon endpoint in %s', atom_url['href']) endpoint = django_salmon.discover_salmon_endpoint(feed) if not endpoint: # try webfinger parsed = urlparse.urlparse(self.resp.target()) # TODO: test missing email email = entry.author_detail.get('email') or '@'.join( (entry.author_detail.name, parsed.netloc)) try: # TODO: always https? resp = common.requests_get( '%s://%s/.well-known/webfinger?resource=acct:%s' % (parsed.scheme, parsed.netloc, email), verify=False) endpoint = django_salmon.get_salmon_replies_link(resp.json()) except requests.HTTPError as e: pass if not endpoint: common.error(self, 'No salmon endpoint found!', status=400) logging.info('Discovered Salmon endpoint %s', endpoint) # construct reply Atom object source_url = self.resp.source() activity = (source_obj if source_obj.get('verb') in source.VERBS_WITH_OBJECT else {'object': source_obj}) entry = atom.activity_to_atom(activity, xml_base=source_url) logging.info('Converted %s to Atom:\n%s', source_url, entry) # sign reply and wrap in magic envelope domain = urlparse.urlparse(source_url).netloc key = MagicKey.get_or_create(domain) logging.info('Using key for %s: %s', domain, key) magic_envelope = magicsigs.magic_envelope( entry, common.CONTENT_TYPE_ATOM, key) logging.info('Sending Salmon slap to %s', endpoint) common.requests_post( endpoint, data=common.XML_UTF8 + magic_envelope, headers={'Content-Type': common.CONTENT_TYPE_MAGIC_ENVELOPE})
def test_bad_inner_xml(self, *mocks): slap = magicsigs.magic_envelope('not xml', common.CONTENT_TYPE_ATOM, self.key) got = self.client.post('/foo.com/salmon', data=slap) self.assertEqual(400, got.status_code)