Пример #1
0
def redir(to):
    """301 redirect to the embedded fully qualified URL.

    e.g. redirects /r/https://foo.com/bar?baz to https://foo.com/bar?baz
    """
    if request.args:
        to += '?' + urllib.parse.urlencode(request.args)
    # some browsers collapse repeated /s in the path down to a single slash.
    # if that happened to this URL, expand it back to two /s.
    to = re.sub(r'^(https?:/)([^/])', r'\1/\2', to)

    if not to.startswith('http://') and not to.startswith('https://'):
        error(f'Expected fully qualified URL; got {to}')

    # check that we've seen this domain before so we're not an open redirect
    domains = set(
        (util.domain_from_link(to), urllib.parse.urlparse(to).hostname))
    for domain in domains:
        if domain and MagicKey.get_by_id(domain):
            logging.info(f'Found MagicKey for domain {domain}')
            break
    else:
        logging.info(f'No user found for any of {domains}; returning 404')
        abort(404)

    # poor man's conneg, only handle single Accept values, not multiple with
    # priorities.
    if request.headers.get('Accept') in (common.CONTENT_TYPE_AS2,
                                         common.CONTENT_TYPE_AS2_LD):
        return convert_to_as2(to)

    # redirect
    logging.info(f'redirecting to {to}')
    return redirect(to, code=301)
Пример #2
0
    def test_actor_handler(self, _, mock_get, __):
        mock_get.return_value = requests_response("""
<body>
<a class="h-card u-url" rel="me" href="/about-me">Mrs. ☕ Foo</a>
</body>
""",
                                                  url='https://foo.com/')

        got = app.get_response('/foo.com')
        mock_get.assert_called_once_with('http://foo.com/',
                                         headers=common.HEADERS,
                                         timeout=util.HTTP_TIMEOUT)
        self.assertEquals(200, got.status_int)
        self.assertEquals(common.CONTENT_TYPE_AS2, got.headers['Content-Type'])
        self.assertEquals(
            {
                '@context': 'https://www.w3.org/ns/activitystreams',
                'type': 'Person',
                'name': 'Mrs. ☕ Foo',
                'summary': '',
                'preferredUsername': '******',
                'id': 'http://localhost/foo.com',
                'url': 'http://localhost/r/https://foo.com/about-me',
                'inbox': 'http://localhost/foo.com/inbox',
                'outbox': 'http://localhost/foo.com/outbox',
                'following': 'http://localhost/foo.com/following',
                'followers': 'http://localhost/foo.com/followers',
                'publicKey': {
                    'id': 'foo.com',
                    'publicKeyPem': MagicKey.get_by_id('foo.com').public_pem(),
                },
            }, json.loads(got.body))
    def get(self, domain):
        tld = domain.split('.')[-1]
        if tld in common.TLD_BLOCKLIST:
            self.error('', status=404)

        mf2 = util.fetch_mf2('http://%s/' % domain, gateway=True,
                             headers=common.HEADERS)
        # logging.info('Parsed mf2 for %s: %s', resp.url, json_dumps(mf2, indent=2))

        hcard = mf2util.representative_hcard(mf2, mf2['url'])
        logging.info('Representative h-card: %s', json_dumps(hcard, indent=2))
        if not hcard:
            self.error("""\
Couldn't find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on %s""" % mf2['url'])

        key = MagicKey.get_or_create(domain)
        obj = self.postprocess_as2(as2.from_as1(microformats2.json_to_object(hcard)),
                                   key=key)
        obj.update({
            'inbox': '%s/%s/inbox' % (self.request.host_url, domain),
            'outbox': '%s/%s/outbox' % (self.request.host_url, domain),
            'following': '%s/%s/following' % (self.request.host_url, domain),
            'followers': '%s/%s/followers' % (self.request.host_url, domain),
        })
        logging.info('Returning: %s', json_dumps(obj, indent=2))

        self.response.headers.update({
            'Content-Type': common.CONTENT_TYPE_AS2,
            'Access-Control-Allow-Origin': '*',
        })
        self.response.write(json_dumps(obj, indent=2))
Пример #4
0
    def test_magic_key_get_or_create(self):
        assert self.key.mod
        assert self.key.public_exponent
        assert self.key.private_exponent

        same = MagicKey.get_or_create('y.z')
        self.assertEquals(same, self.key)
Пример #5
0
    def get(self, domain):
        url = 'http://%s/' % domain
        resp = common.requests_get(url)
        mf2 = mf2py.parse(resp.text, url=resp.url, img_with_alt=True)
        # logging.info('Parsed mf2 for %s: %s', resp.url, json.dumps(mf2, indent=2))

        hcard = mf2util.representative_hcard(mf2, resp.url)
        logging.info('Representative h-card: %s', json.dumps(hcard, indent=2))
        if not hcard:
            common.error(
                self, """\
Couldn't find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on %s"""
                % resp.url)

        key = MagicKey.get_or_create(domain)
        obj = common.postprocess_as2(as2.from_as1(
            microformats2.json_to_object(hcard)),
                                     key=key)
        obj.update({
            'inbox':
            '%s/%s/inbox' % (appengine_config.HOST_URL, domain),
            'outbox':
            '%s/%s/outbox' % (appengine_config.HOST_URL, domain),
            'following':
            '%s/%s/following' % (appengine_config.HOST_URL, domain),
            'followers':
            '%s/%s/followers' % (appengine_config.HOST_URL, domain),
        })
        logging.info('Returning: %s', json.dumps(obj, indent=2))

        self.response.headers.update({
            'Content-Type': common.CONTENT_TYPE_AS2,
            'Access-Control-Allow-Origin': '*',
        })
        self.response.write(json.dumps(obj, indent=2))
Пример #6
0
    def test_actor(self, _, mock_get, __):
        mock_get.return_value = requests_response("""
<body>
<a class="h-card u-url" rel="me" href="/about-me">Mrs. ☕ Foo</a>
</body>
""", url='https://foo.com/', content_type=common.CONTENT_TYPE_HTML)

        got = self.client.get('/foo.com')
        mock_get.assert_called_once_with('http://foo.com/', headers=common.HEADERS,
                                         stream=True, timeout=util.HTTP_TIMEOUT)
        self.assertEqual(200, got.status_code)
        type = got.headers['Content-Type']
        self.assertTrue(type.startswith(common.CONTENT_TYPE_AS2), type)
        self.assertEqual({
            '@context': [
                'https://www.w3.org/ns/activitystreams',
                'https://w3id.org/security/v1',
            ],
            'type' : 'Person',
            'name': 'Mrs. ☕ Foo',
            'summary': '',
            'preferredUsername': '******',
            'id': 'http://localhost/foo.com',
            'url': 'http://localhost/r/https://foo.com/about-me',
            'inbox': 'http://localhost/foo.com/inbox',
            'outbox': 'http://localhost/foo.com/outbox',
            'following': 'http://localhost/foo.com/following',
            'followers': 'http://localhost/foo.com/followers',
            'publicKey': {
                'id': 'foo.com',
                'publicKeyPem': MagicKey.get_by_id('foo.com').public_pem().decode(),
            },
        }, got.json)
Пример #7
0
def actor(domain):
    """Serves /[DOMAIN], fetches its mf2, converts to AS Actor, and serves it."""
    tld = domain.split('.')[-1]
    if tld in common.TLD_BLOCKLIST:
        error('', status=404)

    mf2 = util.fetch_mf2(f'http://{domain}/',
                         gateway=True,
                         headers=common.HEADERS)

    hcard = mf2util.representative_hcard(mf2, mf2['url'])
    logging.info(f'Representative h-card: {json_dumps(hcard, indent=2)}')
    if not hcard:
        error(
            f"Couldn't find a representative h-card (http://microformats.org/wiki/representative-hcard-parsing) on {mf2['url']}"
        )

    key = MagicKey.get_or_create(domain)
    obj = common.postprocess_as2(as2.from_as1(
        microformats2.json_to_object(hcard)),
                                 key=key)
    obj.update({
        'preferredUsername': domain,
        'inbox': f'{request.host_url}{domain}/inbox',
        'outbox': f'{request.host_url}{domain}/outbox',
        'following': f'{request.host_url}{domain}/following',
        'followers': f'{request.host_url}{domain}/followers',
    })
    logging.info(f'Returning: {json_dumps(obj, indent=2)}')

    return (obj, {
        'Content-Type': common.CONTENT_TYPE_AS2,
        'Access-Control-Allow-Origin': '*',
    })
Пример #8
0
    def try_activitypub(self):
        """Returns True if we attempted ActivityPub delivery, False otherwise."""
        targets = self._activitypub_targets()
        if not targets:
            return False

        key = MagicKey.get_or_create(self.source_domain)
        error = None
        last_success = None

        # TODO: collect by inbox, add 'to' fields, de-dupe inboxes and recipients

        for resp, inbox in targets:
            target_obj = json_loads(
                resp.target_as2) if resp.target_as2 else None
            source_activity = self.postprocess_as2(as2.from_as1(
                self.source_obj),
                                                   target=target_obj,
                                                   key=key)

            if resp.status == 'complete':
                source_activity['type'] = 'Update'

            try:
                last = activitypub.send(source_activity, inbox,
                                        self.source_domain)
                resp.status = 'complete'
                last_success = last
            except BaseException as e:
                error = e
                resp.status = 'error'

            resp.put()

        # Pass the AP response status code and body through as our response
        if last_success:
            self.response.status_int = last_success.status_code
            self.response.write(last_success.text)
        elif isinstance(error, requests.HTTPError):
            self.response.status_int = error.status_code
            self.response.write(error.text)
        else:
            self.response.write(str(error))

        return bool(last_success)
Пример #9
0
def send(activity, inbox_url, user_domain):
    """Sends an ActivityPub request to an inbox.

    Args:
      activity: dict, AS2 activity
      inbox_url: string
      user_domain: string, domain of the bridgy fed user sending the request

    Returns:
      requests.Response
    """
    logging.info(
        'Sending AP request from {user_domain}: {json_dumps(activity, indent=2)}'
    )

    # prepare HTTP Signature (required by Mastodon)
    # https://w3c.github.io/activitypub/#authorization
    # https://tools.ietf.org/html/draft-cavage-http-signatures-07
    # https://github.com/tootsuite/mastodon/issues/4906#issuecomment-328844846
    acct = 'acct:%s@%s' % (user_domain, user_domain)
    key = MagicKey.get_or_create(user_domain)
    auth = HTTPSignatureAuth(secret=key.private_pem(),
                             key_id=acct,
                             algorithm='rsa-sha256',
                             sign_header='signature',
                             headers=('Date', 'Digest', 'Host'))

    # deliver to inbox
    body = json_dumps(activity).encode()
    headers = {
        'Content-Type': common.CONTENT_TYPE_AS2,
        # required for HTTP Signature
        # https://tools.ietf.org/html/draft-cavage-http-signatures-07#section-2.1.3
        'Date':
        datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'),
        # required by Mastodon
        # https://github.com/tootsuite/mastodon/pull/14556#issuecomment-674077648
        'Digest': 'SHA-256=' + b64encode(sha256(body).digest()).decode(),
        'Host': util.domain_from_link(inbox_url),
    }
    return common.requests_post(inbox_url,
                                data=body,
                                auth=auth,
                                headers=headers)
Пример #10
0
def send(activity, inbox_url, user_domain):
    """Sends an ActivityPub request to an inbox.

    Args:
      activity: dict, AS2 activity
      inbox_url: string
      user_domain: string, domain of the bridgy fed user sending the request

    Returns:
      requests.Response
    """
    logging.info('Sending AP request from %s: %s', user_domain,
                 json.dumps(activity, indent=2))

    # prepare HTTP Signature (required by Mastodon)
    # https://w3c.github.io/activitypub/#authorization-lds
    # https://tools.ietf.org/html/draft-cavage-http-signatures-07
    # https://github.com/tootsuite/mastodon/issues/4906#issuecomment-328844846
    acct = 'acct:%s@%s' % (user_domain, user_domain)
    key = MagicKey.get_or_create(user_domain)
    auth = HTTPSignatureAuth(secret=key.private_pem(),
                             key_id=acct,
                             algorithm='rsa-sha256')

    # deliver to inbox
    headers = {
        'Content-Type': common.CONTENT_TYPE_AS2,
        # required for HTTP Signature
        # https://tools.ietf.org/html/draft-cavage-http-signatures-07#section-2.1.3
        'Date':
        datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'),
    }
    return common.requests_post(inbox_url,
                                json=activity,
                                auth=auth,
                                headers=headers)
Пример #11
0
    def try_activitypub(self):
        """Attempts ActivityPub delivery.

        Returns Flask response (string body or tuple) if we succeeded or failed,
        None if ActivityPub was not available.
        """
        targets = self._activitypub_targets()
        if not targets:
            return None

        key = MagicKey.get_or_create(self.source_domain)
        error = None
        last_success = None

        # TODO: collect by inbox, add 'to' fields, de-dupe inboxes and recipients

        for resp, inbox in targets:
            target_obj = json_loads(
                resp.target_as2) if resp.target_as2 else None
            source_activity = common.postprocess_as2(as2.from_as1(
                self.source_obj),
                                                     target=target_obj,
                                                     key=key)

            if resp.status == 'complete':
                if resp.source_mf2:

                    def content(mf2):
                        items = mf2.get('items')
                        if items:
                            return microformats2.first_props(
                                items[0].get('properties')).get('content')

                    orig_content = content(json_loads(resp.source_mf2))
                    new_content = content(self.source_mf2)
                    if orig_content and new_content and orig_content == new_content:
                        msg = f'Skipping; new content is same as content published before at {resp.updated}'
                        logging.info(msg)
                        return msg

                source_activity['type'] = 'Update'

            try:
                last = activitypub.send(source_activity, inbox,
                                        self.source_domain)
                resp.status = 'complete'
                last_success = last
            except BaseException as e:
                error = e
                resp.status = 'error'

            resp.put()

        # Pass the AP response status code and body through as our response
        if last_success:
            return last_success.text or 'Sent!', last_success.status_code
        elif isinstance(error, BadGateway):
            raise error
        elif isinstance(error, requests.HTTPError):
            return str(error), error.status_code
        else:
            return str(error)
Пример #12
0
    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
Пример #13
0
 def setUp(self):
     super(MagicKeyTest, self).setUp()
     self.key = MagicKey.get_or_create('y.z')
Пример #14
0
    def try_activitypub(self):
        source = util.get_required_param(self, 'source')

        # fetch source page, convert to ActivityStreams
        source_resp = common.requests_get(source)
        source_url = source_resp.url or source
        source_mf2 = mf2py.parse(source_resp.text, url=source_url)
        # logging.debug('Parsed mf2 for %s: %s', source_resp.url, json.dumps(source_mf2, indent=2))

        entry = mf2util.find_first_entry(source_mf2, ['h-entry'])
        logging.info('First entry: %s', json.dumps(entry, indent=2))
        # make sure it has url, since we use that for AS2 id, which is required
        # for ActivityPub.
        props = entry.setdefault('properties', {})
        if not props.get('url'):
            props['url'] = [source_url]

        source_obj = microformats2.json_to_object(entry, fetch_mf2=True)
        logging.info('Converted to AS: %s', json.dumps(source_obj, indent=2))

        # fetch target page as AS object. target is first in-reply-to, like-of,
        # or repost-of, *not* target query param.)
        target = util.get_url(util.get_first(source_obj, 'inReplyTo') or
                              util.get_first(source_obj, 'object'))
        if not target:
            common.error(self, 'No u-in-reply-to, u-like-of, or u-repost-of '
                         'found in %s' % source_url)

        try:
            target_resp = common.get_as2(target)
        except (requests.HTTPError, exc.HTTPBadGateway) as e:
            if (e.response.status_code // 100 == 2 and
                common.content_type(e.response).startswith('text/html')):
                self.resp = Response.get_or_create(
                    source=source_url, target=e.response.url or target,
                    direction='out', source_mf2=json.dumps(source_mf2))
                return self.send_salmon(source_obj, target_resp=e.response)
            raise

        target_url = target_resp.url or target
        self.resp = Response.get_or_create(
            source=source_url, target=target_url, direction='out',
            protocol='activitypub', source_mf2=json.dumps(source_mf2))

        # find actor's inbox
        target_obj = target_resp.json()
        inbox_url = target_obj.get('inbox')

        if not inbox_url:
            # TODO: test actor/attributedTo and not, with/without inbox
            actor = target_obj.get('actor') or target_obj.get('attributedTo')
            if isinstance(actor, dict):
                inbox_url = actor.get('inbox')
                actor = actor.get('url')
            if not inbox_url and not actor:
                common.error(self, 'Target object has no actor or attributedTo URL')

        if not inbox_url:
            # fetch actor as AS object
            actor = common.get_as2(actor).json()
            inbox_url = actor.get('inbox')

        if not inbox_url:
            # TODO: probably need a way to save errors like this so that we can
            # return them if ostatus fails too.
            # common.error(self, 'Target actor has no inbox')
            return self.send_salmon(source_obj, target_resp=target_resp)

        # convert to AS2
        source_domain = urlparse.urlparse(source_url).netloc
        key = MagicKey.get_or_create(source_domain)
        source_activity = common.postprocess_as2(
            as2.from_as1(source_obj), target=target_obj, key=key)

        if self.resp.status == 'complete':
            source_activity['type'] = 'Update'

        # prepare HTTP Signature (required by Mastodon)
        # https://w3c.github.io/activitypub/#authorization-lds
        # https://tools.ietf.org/html/draft-cavage-http-signatures-07
        # https://github.com/tootsuite/mastodon/issues/4906#issuecomment-328844846
        acct = 'acct:%s@%s' % (source_domain, source_domain)
        auth = HTTPSignatureAuth(secret=key.private_pem(), key_id=acct,
                                 algorithm='rsa-sha256')

        # deliver source object to target actor's inbox.
        headers = {
            'Content-Type': common.CONTENT_TYPE_AS2,
            # required for HTTP Signature
            # https://tools.ietf.org/html/draft-cavage-http-signatures-07#section-2.1.3
            'Date': datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'),
        }
        inbox_url = urlparse.urljoin(target_url, inbox_url)
        resp = common.requests_post(inbox_url, json=source_activity, auth=auth,
                                    headers=headers)
        self.response.status_int = resp.status_code
        if resp.status_code == 202:
            self.response.write('202 response! If this is Mastodon 1.x, their '
                                'signature verification probably failed. :(\n')
        self.response.write(resp.text)
Пример #15
0
    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})
Пример #16
0
 def setUp(self):
     super().setUp()
     MagicKey.get_or_create('foo.com')
Пример #17
0
    def setUp(self):
        super(WebmentionTest, self).setUp()
        self.key = MagicKey.get_or_create('a')

        self.orig_html_as2 = requests_response("""\
<html>
<meta>
<link href='http://orig/atom' rel='alternate' type='application/atom+xml'>
<link href='http://orig/as2' rel='alternate' type='application/activity+json'>
</meta>
</html>
""",
                                               url='http://orig/post',
                                               content_type=CONTENT_TYPE_HTML)
        self.orig_html_atom = requests_response("""\
<html>
<meta>
<link href='http://orig/atom' rel='alternate' type='application/atom+xml'>
</meta>
</html>
""",
                                                url='http://orig/post',
                                                content_type=CONTENT_TYPE_HTML)
        self.orig_atom = requests_response("""\
<?xml version="1.0"?>
<entry xmlns="http://www.w3.org/2005/Atom">
  <id>tag:fed.brid.gy,2017-08-22:orig-post</id>
  <link rel="salmon" href="http://orig/salmon"/>
  <content type="html">baz ☕ baj</content>
</entry>
""")
        self.orig_as2_data = {
            '@context': ['https://www.w3.org/ns/activitystreams'],
            'type': 'Article',
            'id': 'tag:orig,2017:as2',
            'content': 'Lots of ☕ words...',
            'actor': {
                'url': 'http://orig/author'
            },
            'to': ['http://orig/recipient'],
            'cc': ['http://orig/bystander', AS2_PUBLIC_AUDIENCE],
        }
        self.orig_as2 = requests_response(self.orig_as2_data,
                                          url='http://orig/as2',
                                          content_type=CONTENT_TYPE_AS2 +
                                          '; charset=utf-8')

        self.reply_html = """\
<html>
<body>
<div class="h-entry">
<a class="u-url" href="http://a/reply"></a>
<p class="e-content p-name">
<a class="u-in-reply-to" href="http://orig/post">foo ☕ bar</a>
<a href="http://localhost/"></a>
</p>
<a class="p-author h-card" href="http://orig">Ms. ☕ Baz</a>
</div>
</body>
</html>
"""
        self.reply = requests_response(self.reply_html,
                                       content_type=CONTENT_TYPE_HTML)
        self.reply_mf2 = mf2py.parse(self.reply_html, url='http://a/reply')

        self.repost_html = REPOST_HTML
        self.repost = requests_response(self.repost_html,
                                        content_type=CONTENT_TYPE_HTML)
        self.repost_mf2 = mf2py.parse(self.repost_html, url='http://a/repost')
        self.repost_as2 = REPOST_AS2

        self.like_html = """\
<html>
<body class="h-entry">
<a class="u-url" href="http://a/like"></a>
<a class="u-like-of" href="http://orig/post"></a>
<!--<a class="u-like-of p-name" href="http://orig/post">liked!</a>-->
<a class="p-author h-card" href="http://orig">Ms. ☕ Baz</a>
<a href="http://localhost/"></a>
</body>
</html>
"""
        self.like = requests_response(self.like_html,
                                      content_type=CONTENT_TYPE_HTML)
        self.like_mf2 = mf2py.parse(self.like_html, url='http://a/like')

        self.actor = requests_response(
            {
                'objectType': 'person',
                'displayName': 'Mrs. ☕ Foo',
                'url': 'https://foo.com/about-me',
                'inbox': 'https://foo.com/inbox',
            },
            content_type=CONTENT_TYPE_AS2)
        self.activitypub_gets = [self.reply, self.orig_as2, self.actor]

        self.as2_create = {
            '@context': 'https://www.w3.org/ns/activitystreams',
            'type': 'Create',
            'object': {
                '@context':
                'https://www.w3.org/ns/activitystreams',
                'type':
                'Note',
                'id':
                'http://localhost/r/http://a/reply',
                'url':
                'http://localhost/r/http://a/reply',
                'name':
                'foo ☕ bar',
                'content':
                '<a class="u-in-reply-to" href="http://orig/post">foo ☕ bar</a>\n<a href="http://localhost/"></a>',
                'inReplyTo':
                'tag:orig,2017:as2',
                'cc': [
                    AS2_PUBLIC_AUDIENCE,
                    'http://orig/author',
                    'http://orig/recipient',
                    'http://orig/bystander',
                ],
                'attributedTo': [{
                    'type': 'Person',
                    'id': 'http://localhost/orig',
                    'url': 'http://localhost/r/http://orig',
                    'preferredUsername': '******',
                    'name': 'Ms. ☕ Baz',
                }],
                'tag': [{
                    'type': 'Mention',
                    'href': 'http://orig/author',
                }],
            },
        }
        self.as2_update = copy.deepcopy(self.as2_create)
        self.as2_update['type'] = 'Update'

        self.follow_html = """\
<html>
<body class="h-entry">
<a class="u-url" href="http://a/follow"></a>
<a class="u-follow-of" href="http://followee"></a>
<a class="p-author h-card" href="https://orig">Ms. ☕ Baz</a>
<a href="http://localhost/"></a>
</body>
</html>
"""
        self.follow = requests_response(self.follow_html,
                                        content_type=CONTENT_TYPE_HTML)
        self.follow_mf2 = mf2py.parse(self.follow_html, url='http://a/follow')
        self.follow_as2 = {
            '@context': 'https://www.w3.org/ns/activitystreams',
            'type': 'Follow',
            'id': 'http://localhost/r/http://a/follow',
            'url': 'http://localhost/r/http://a/follow',
            'object': 'http://followee',
            'actor': {
                'id': 'http://localhost/orig',
                'name': 'Ms. ☕ Baz',
                'preferredUsername': '******',
                'type': 'Person',
                'url': 'http://localhost/r/https://orig',
            },
            'cc': ['https://www.w3.org/ns/activitystreams#Public'],
        }

        self.actor = requests_response(
            {
                'objectType': 'person',
                'displayName': 'Mrs. ☕ Foo',
                'url': 'https://foo.com/about-me',
                'inbox': 'https://foo.com/inbox',
            },
            content_type=CONTENT_TYPE_AS2)
        self.activitypub_gets = [self.reply, self.orig_as2, self.actor]

        self.create_html = """\
<html>
<body class="h-entry">
<a class="u-url" href="http://orig/post"></a>
<p class="e-content p-name">hello i am a post</p>
<a class="p-author h-card" href="https://orig">Ms. ☕ Baz</a>
<a href="http://localhost/"></a>
</body>
</html>
"""
        self.create = requests_response(self.create_html,
                                        content_type=CONTENT_TYPE_HTML)
        self.create_mf2 = mf2py.parse(self.create_html, url='http://a/create')
        self.create_as2 = {
            '@context': 'https://www.w3.org/ns/activitystreams',
            'type': 'Create',
            'object': {
                '@context':
                'https://www.w3.org/ns/activitystreams',
                'type':
                'Note',
                'id':
                'http://localhost/r/http://orig/post',
                'url':
                'http://localhost/r/http://orig/post',
                'name':
                'hello i am a post',
                'content':
                'hello i am a post',
                'attributedTo': [{
                    'type': 'Person',
                    'id': 'http://localhost/orig',
                    'url': 'http://localhost/r/https://orig',
                    'name': 'Ms. ☕ Baz',
                    'preferredUsername': '******',
                }],
                'cc': ['https://www.w3.org/ns/activitystreams#Public'],
            },
        }

        self.actor = requests_response(
            {
                'objectType': 'person',
                'displayName': 'Mrs. ☕ Foo',
                'url': 'https://foo.com/about-me',
                'inbox': 'https://foo.com/inbox',
            },
            content_type=CONTENT_TYPE_AS2)
        self.activitypub_gets = [self.reply, self.orig_as2, self.actor]
Пример #18
0
 def setUp(self):
     super(SalmonTest, self).setUp()
     self.key = MagicKey.get_or_create('alice')
Пример #19
0
 def setUp(self):
     super().setUp()
     self.key = MagicKey.get_or_create('alice')