Ejemplo n.º 1
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))
Ejemplo n.º 2
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': '*',
    })
Ejemplo n.º 3
0
 def test_postprocess_as2_multiple_in_reply_tos(self):
     self.assertEqual({
         'id': 'http://localhost/r/xyz',
         'inReplyTo': 'foo',
     }, common.postprocess_as2({
         'id': 'xyz',
         'inReplyTo': ['foo', 'bar'],
     }))
Ejemplo n.º 4
0
def convert_to_as2(url):
    """Fetch a URL as HTML, convert it to AS2, and return it.

    Currently mainly for Pixelfed.
    https://github.com/snarfed/bridgy-fed/issues/39
    """
    mf2 = util.fetch_mf2(url)
    entry = mf2util.find_first_entry(mf2, ['h-entry'])
    logging.info(f"Parsed mf2 for {mf2['url']}: {json_dumps(entry, indent=2)}")

    obj = common.postprocess_as2(
        as2.from_as1(microformats2.json_to_object(entry)))
    logging.info(f'Returning: {json_dumps(obj, indent=2)}')

    return obj, {
        'Content-Type': common.CONTENT_TYPE_AS2,
        'Access-Control-Allow-Origin': '*',
    }
Ejemplo n.º 5
0
    def convert_to_as2(self, url):
        """Fetch a URL as HTML, convert it to AS2, and return it.

        Currently mainly for Pixelfed.
        https://github.com/snarfed/bridgy-fed/issues/39
        """
        resp = common.requests_get(url)
        mf2 = mf2py.parse(resp.text, url=resp.url, img_with_alt=True)
        entry = mf2util.find_first_entry(mf2, ['h-entry'])
        logging.info('Parsed mf2 for %s: %s', resp.url,
                     json.dumps(entry, indent=2))

        obj = common.postprocess_as2(
            as2.from_as1(microformats2.json_to_object(entry)))
        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))
Ejemplo n.º 6
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
        delivered = set()  # inboxes we've delivered to

        # 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':
                source_activity['type'] = 'Update'

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

            resp.put()

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

        return not error
Ejemplo n.º 7
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)
Ejemplo n.º 8
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)