Ejemplo n.º 1
0
def get_public_key(key_id):
    '''
        Takes the "keyId" field of a request and
        uses resolve_ap_object-actor to fetch the public key
        of the specified actor.
    '''
    actor = resolve_ap_object(key_id)

    if not actor or not actor.get('publicKey'):
        return error(
            'An error occurred while attempting to fetch the public key of the inbound actor.',
            400)

    public_key_wrapper = actor.get('publicKey')

    if public_key_wrapper.get('id') != key_id:
        return error('Keys don\'t match', 400)

    public_key_string = public_key_wrapper.get('publicKeyPem')

    if not public_key_string:
        return error('Public key not found.')

    public_key = RSA.importKey(bytes(public_key_string, 'utf-8'))

    return public_key
Ejemplo n.º 2
0
def route_shared_inbox():
    if request.method == 'GET':
        return get_inbox(personalized=False)
    elif request.method == 'POST':
        activity = request.get_json()
        obj = resolve_ap_object(activity['object'])

        return new_ob_object(activity, obj)
Ejemplo n.º 3
0
def handle_inbound_follow(activity, obj):
    '''
        Handles the side effects of an incoming Follow activity.
        Returns: Flask response if an error occurs, None otherwise
    '''

    api_url = os.environ['API_URL']

    if obj['id'].find(f'{api_url}/actors/') < 0:
        return error('Invalid actor ID')

    local_actor_name = obj['id'].replace(f'{api_url}/actors/', '').lower()

    leader = db.session.query(Actor).filter(
        db.func.lower(Actor.username) == local_actor_name).first()

    if leader is None:
        return error('Actor not found', 404)

    follower = resolve_ap_object(activity['actor'])

    follower_shared_inbox = None
    follower_inbox = follower['inbox']

    if 'endpoints' in follower:
        if 'sharedInbox' in follower['endpoints']:
            follower_shared_inbox = follower['endpoints']['sharedInbox']

    accept_activity = Accept()
    accept_activity.set_actor(leader)
    accept_activity.set_object(activity)

    db.session.add(accept_activity)
    db.session.flush()

    message_body = accept_activity.to_dict()
    message_body['object'] = activity

    try:
        signed_request(leader, message_body, url=follower_inbox)
    except:
        return error('An error occurred while processing that follow request.',
                     400)

    new_followed_by = FollowedBy(leader.id, follower['id'], follower_inbox,
                                 follower_shared_inbox)
    db.session.add(new_followed_by)

    follower_username = follower['id']
    if 'preferredUsername' in follower:
        follower_username = follower['preferredUsername']

    db.session.add(
        Notification(leader, f'{follower_username} has followed you',
                     'Follow'))

    return None
Ejemplo n.º 4
0
def handle_follow(inbound_json, actor, base_activity, base_object, is_local):
    '''
        Handles the various oddities associated with the correspondance nature of the Follow activity.
    '''
    leader = resolve_ap_object(inbound_json['object'])

    existing_follow = db.session.query(Following).filter(
        db.and_(Following.follower_id == actor.id,
                Following.leader == leader['id'])).first()

    if existing_follow is not None and existing_follow.approved is True:
        return error('You are already following this actor.')

    new_follow = Following(actor.id,
                           leader['id'],
                           leader['followers'],
                           approved=is_local)
    db.session.add(new_follow)

    # Due to the correspondance nature of the Follow activity, it has some very unusual requirements.
    # We need to detect if the incoming Follow activity is targeting a local user. If it is, we don't need
    # To deliver server-to-server messages about this transaction. Attempting to do so would cause problems
    # resulting from uncomitted database transactions and be a waste of resources.
    if is_local:
        local_leader = db.session.query(Actor).filter(
            db.func.lower(Actor.username) == db.func.lower(
                inbound_json['object'].replace(
                    f'{os.environ["API_URL"]}/actors/', ''))).first()
        if local_leader is None:
            return make_response('Actor not found', 404)
        actor_dict = actor.to_dict()
        new_followed_by = FollowedBy(
            local_leader.id,
            actor_dict['id'],
            actor_dict['inbox'],
            follower_shared_inbox=actor_dict['endpoints']['sharedInbox'],
            approved=True)
        db.session.add(
            Notification(
                local_leader,
                f'{actor_dict["preferredUsername"]} has followed you.',
                'Follow'))
        db.session.add(new_followed_by)
        db.session.commit()
    else:
        db.session.commit(
        )  # This is required so when we get an Accept activity back before the end of this request, we're able to find the Follow activity
        try:
            signed_request(actor, base_activity.to_dict(), leader['inbox'])
        except:
            return error(
                'Your follow request was not able to be delivered to that server.'
            )

    return make_response('', 200)
Ejemplo n.º 5
0
def route_actor_inbox(actor_name):

    if request.method == 'POST':
        activity = request.get_json()
        recipient = db.session.query(Actor).filter_by(
            username=actor_name.lower()).first()

        if recipient is None:
            return error('The specified actor could not be found.', 404)

        obj = resolve_ap_object(request.get_json().get('object'))

        return new_ob_object(activity, obj, recipient)

    elif request.method == 'GET':
        return get_actor_inbox(actor_name=actor_name)
Ejemplo n.º 6
0
def route_undo_follow(user):

    from vagabond.crypto import signed_request

    follower = user.primary_actor
    leader = resolve_ap_object(request.get_json()['leader'])

    follow_activity = db.session.query(Follow).filter(
        Follow.internal_actor_id == follower.id,
        Follow.external_object_id == leader['id']).first()
    if follow_activity is None:
        print('@@@@@@@@@@@@@@@@@@@@@@@')
        return error(
            'It doesn'
            't appear that you follow that user :thonking:', 404)

    following = db.session.query(Following).filter(
        Following.leader == leader['id']).filter(
            Following.follower_id == follower.id).first()
    if following is None:
        return error(
            'It doesn'
            't appear that you follow that user :thonking:', 404)

    api_url = os.environ['API_URL']
    uuid = uuid4()

    signed_request(
        follower, {
            '@context': 'https://www.w3.org/ns/activitystreams',
            'type': 'Undo',
            'id': f'{api_url}/unfollowActivity/{uuid}',
            'actor': follower.to_dict()['id'],
            'object': {
                'type': 'Follow',
                'actor': follower.to_dict()['id'],
                'object': leader['id'],
                'id': follow_activity.to_dict()['id']
            }
        }, leader['inbox'])

    db.session.delete(follow_activity)
    db.session.delete(following)
    db.session.commit()

    return make_response('', 200)
Ejemplo n.º 7
0
def follow(actor, follow_activity):
    '''
        actor: Actor model
        follow_activity: Dictionary representation of the new follow request
    '''

    leader = resolve_ap_object(follow_activity['object'])

    existing_follow = db.session.query(Following).filter(
        db.and_(Following.follower_id == actor.id,
                Following.leader == leader['id'])).first()

    if existing_follow is not None:
        if existing_follow.approved is True:
            return error('You are already following this actor.')

        db.session.delete(existing_follow)

    new_activity = Follow()
    new_activity.set_actor(actor)
    new_activity.set_object(follow_activity['object'])
    db.session.add(new_activity)
    db.session.flush()

    new_follow = Following(actor.id, leader['id'], leader['followers'])
    db.session.add(new_follow)

    response = signed_request(actor, new_activity.to_dict(), leader['inbox'])

    db.session.commit()

    if response.status_code >= 400:

        return error('Something went wrong :(')

    return make_response('', 200)
Ejemplo n.º 8
0
def deliver(actor, message):
    '''
        Delivers the specified message to the recipients listed in the to, bto, cc, and bcc fields. 
    '''

    api_url = os.environ['API_URL']

    keys = ['to', 'bto', 'cc', 'bcc']
    recipients = []
    for key in keys:
        if key in message:
            if isinstance(message[key], str):
                message[key] = [message[key]]
            for value in message[key]:
                recipients.append(value)

    all_inboxes = []

    for recipient in recipients:
        try:
            # possibility 1: local delivery of some kind. If delivery is to local actor, do nothing. If delivery is to local followers collection, distribute to followers.
            if recipient.replace(f'{api_url}/actors/', '') != recipient:
                splits = recipient.replace(f'{api_url}/actors/', '').split('/')

                if len(splits) == 2 and splits[1] == 'followers':
                    leader = db.session.query(Actor).filter(
                        db.func.lower(Actor.username) == db.func.lower(
                            splits[0])).first()
                    if leader is None:
                        continue

                    shared_inboxes = db.session.query(
                        FollowedBy.follower_shared_inbox.distinct()).filter(
                            FollowedBy.leader_id == leader.id,
                            FollowedBy.follower_shared_inbox != None).all()
                    for inbox in shared_inboxes:
                        all_inboxes.append(inbox[0])

                    unique_inboxes = db.session.query(
                        FollowedBy.follower_inbox.distinct()).filter(
                            FollowedBy.leader_id == leader.id,
                            FollowedBy.follower_shared_inbox == None).all()
                    for inbox in unique_inboxes:
                        all_inboxes.append(inbox[0])

            # possibility 2: external delivery of some kind
            else:

                actor_types = [
                    'Application', 'Group', 'Organization', 'Person', 'Service'
                ]

                recipient_object = resolve_ap_object(recipient)

                if recipient_object is None:
                    continue

                if recipient_object['type'] in actor_types:

                    if 'endpoints' in recipient_object and 'sharedInbox' in recipient_object[
                            'endpoints'] and recipient_object['endpoints'][
                                'sharedInbox'] not in all_inboxes:
                        all_inboxes.append(
                            recipient_object['endpoints']['sharedInbox'])
                    elif 'inbox' in recipient_object:
                        all_inboxes.append(recipient_object['inbox'])

                elif (recipient_object['type'] == 'OrderedCollection'
                      or recipient_object['type'] == 'Collection'):

                    # We have a page with the items all in one place
                    if 'items' in recipient_object:
                        for item in recipient_object['items']:
                            if isinstance(item, str):
                                if item not in all_inboxes:
                                    all_inboxes.append(item)
                            elif isinstance(item, dict):
                                if 'endpoints' in item and 'sharedInbox' in item[
                                        'endpoints']:
                                    shared_inbox = item['endpoints'][
                                        'sharedInbox']
                                    if shared_inbox not in all_inboxes:
                                        all_inboxes.append(shared_inbox)
                                elif 'inbox' in item:
                                    if item['inbox'] not in all_inboxes:
                                        all_inboxes.append(item['inbox'])

                    # Traditional segmented OrderedCollection
                    else:

                        next_page = resolve_ap_object(
                            recipient_object['first'])
                        iteration = 0

                        while next_page is not None and iteration < 10:

                            for item in next_page['items']:
                                if isinstance(item,
                                              str) and item not in all_inboxes:
                                    all_inboxes.append(item)
                                elif isinstance(
                                        item, dict
                                ) and 'id' in item and 'type' in item and item[
                                        'type'] in actor_types:
                                    all_inboxes.append(item)

                            iteration = iteration + 1
                            if 'next' in next_page:
                                next_page = resolve_ap_object(
                                    next_page['next'])
                            else:
                                next_page = None

                    #TODO: Delivery to external collections

        except Exception as e:
            print(e)

    for inbox in all_inboxes:
        # Don't deliver messages to ourselves!
        if inbox.replace(os.environ['API_URL'], '') == inbox:
            try:
                signed_request(actor, message, url=inbox)
            except Exception as e:
                print(e)
                print(
                    f'Could not deliver message to the following inbox: {inbox}'
                )

                app.logger.error(
                    f'Could not deliver message to the following inbox: {inbox}'
                )
Ejemplo n.º 9
0
def webfinger_federated():
    '''
        Used for looking actors on other servers
        via their WebFinger interface.
    '''

    if 'type' not in request.args:
        return error('Invalid request')

    #webfinger proxy
    if request.args['type'] == 'webfinger':
        username = request.args.get('username')
        hostname = request.args.get('hostname')
        if not username or not hostname:
            return error('Invalid request')

        try:
            response = requests.get(
                f'https://{hostname}/.well-known/webfinger?resource=acct:{username}@{hostname}'
            )
            if response.status_code == 404:
                return error('User not found.', 404)
            return make_response(response.json(), 200)
        except Exception as e:
            return error(
                'An error occurred while attempting to look up the specified user.'
            )

    #Actor proxy
    elif request.args['type'] == 'actor':
        username = request.args.get('username')
        hostname = request.args.get('hostname')
        try:
            response = requests.get(
                f'https://{hostname}/.well-known/webfinger?resource=acct:{username}@{hostname}'
            )
            if response.status_code >= 300:
                return error(response.text, response.status_code)

            if 'links' in response.json() and isinstance(
                    response.json()['links'], list):
                for link in response.json()['links']:
                    if 'rel' in link and link['rel'] == 'self':
                        actor_data = resolve_ap_object(link['href'])
                        return make_response(actor_data, 200)
                return error(
                    f'Invalid WebFinger response for actor @{username}@{hostname}: no "self" link. '
                )
            elif 'href' in response.json():
                actor_data = resolve_ap_object(response.json()['href'])
                return make_response(actor_data, 200)
            else:

                return error(
                    f'Failed to look up actor @{username}@{hostname}: server sent invalid response.'
                )

        except Exception as e:
            print(e)
            return error(
                'An error occurred while attempting to look up the specified actor.'
            )