예제 #1
0
def test_validate_handle():
    assert validate_handle("*****@*****.**")
    assert validate_handle("*****@*****.**")
    assert validate_handle("*****@*****.**")
    assert validate_handle("[email protected]:3000")
    assert not validate_handle("@bar.com")
    assert not validate_handle("foo@b/ar.com")
    assert not validate_handle("foo@bar")
    assert not validate_handle("fo/[email protected]")
    assert not validate_handle("foobar.com")
    assert not validate_handle("foo@bar,com")
예제 #2
0
def retrieve_and_parse_content(
        id: str, guid: str, handle: str, entity_type: str, sender_key_fetcher: Callable[[str], str]=None,
):
    """Retrieve remote content and return an Entity class instance.

    This is basically the inverse of receiving an entity. Instead, we fetch it, then call "handle_receive".

    :param sender_key_fetcher: Function to use to fetch sender public key. If not given, network will be used
        to fetch the profile and the key. Function must take handle as only parameter and return a public key.
    :returns: Entity object instance or ``None``
    """
    if not validate_handle(handle):
        return
    _username, domain = handle.split("@")
    url = get_fetch_content_endpoint(domain, entity_type.lower(), guid)
    document, status_code, error = fetch_document(url)
    if status_code == 200:
        request = RequestType(body=document)
        _sender, _protocol, entities = handle_receive(request, sender_key_fetcher=sender_key_fetcher)
        if len(entities) > 1:
            logger.warning("retrieve_and_parse_content - more than one entity parsed from remote even though we"
                           "expected only one! ID %s", guid)
        if entities:
            return entities[0]
        return
    elif status_code == 404:
        logger.warning("retrieve_and_parse_content - remote content %s not found", guid)
        return
    if error:
        raise error
    raise Exception("retrieve_and_parse_content - unknown problem when fetching document: %s, %s, %s" % (
        document, status_code, error,
    ))
예제 #3
0
def retrieve_remote_content(
    id: str,
    guid: str = None,
    handle: str = None,
    entity_type: str = None,
    sender_key_fetcher: Callable[[str], str] = None,
):
    """Retrieve remote content and return an Entity object.

    ``sender_key_fetcher`` is an optional function to use to fetch sender public key. If not given, network will be used
    to fetch the profile and the key. Function must take federation id as only parameter and return a public key.
    """
    if handle and validate_handle(handle):
        protocol_name = "diaspora"
        if not guid:
            guid = id
    else:
        protocol_name = identify_protocol_by_id(id).PROTOCOL_NAME
    utils = importlib.import_module("federation.utils.%s" % protocol_name)
    return utils.retrieve_and_parse_content(
        id=id,
        guid=guid,
        handle=handle,
        entity_type=entity_type,
        sender_key_fetcher=sender_key_fetcher,
    )
예제 #4
0
    def save(self, *args, **kwargs):
        if not self.uuid:
            self.uuid = uuid4()
        if not self.pk and self.is_local:
            if not self.guid:
                self.guid = str(self.uuid)
            if not self.fid:
                self.fid = self.url

        if not self.fid and not self.handle:
            raise ValueError("Profile must have either a fid or a handle")

        if self.handle:
            # Ensure handle is *always* lowercase
            self.handle = self.handle.lower()
            if not validate_handle(self.handle):
                raise ValueError("Not a valid handle")

        # Set default pony images if image urls are empty
        if not self.image_url_small or not self.image_url_medium or not self.image_url_large:
            ponies = get_pony_urls()
            for idx, attr in enumerate(["image_url_large", "image_url_medium", "image_url_small"]):
                if not getattr(self, attr, None):
                    setattr(self, attr, ponies[idx])

        # Ensure keys are converted to str before saving
        self.rsa_private_key = decode_if_bytes(self.rsa_private_key)
        self.rsa_public_key = decode_if_bytes(self.rsa_public_key)

        super().save(*args, **kwargs)
예제 #5
0
파일: views.py 프로젝트: jcooter/socialhome
    def get(self, request, *args, **kwargs):
        """See if we have a direct match. If so redirect, if not, search.

        Try fetching a remote profile if the search term is a handle.
        """
        q = safe_text(request.GET.get("q"))
        if q:
            q = q.strip().lower()
        if validate_handle(q):
            profile = None
            try:
                profile = Profile.objects.visible_for_user(
                    request.user).get(handle=q)
            except Profile.DoesNotExist:
                # Try a remote search
                try:
                    remote_profile = retrieve_remote_profile(q)
                except (AttributeError, ValueError,
                        xml.parsers.expat.ExpatError):
                    # Catch various errors parsing the remote profile
                    return super().get(request, *args, **kwargs)
                if remote_profile:
                    profile = Profile.from_remote_profile(remote_profile)
            if profile:
                return redirect(
                    reverse("users:profile-detail",
                            kwargs={"guid": profile.guid}))
        try:
            return super().get(request, *args, **kwargs)
        except QueryError:
            # Re-render the form
            messages.warning(
                self.request,
                _("Search string is invalid, please try another one."))
            return HttpResponseRedirect(self.get_success_url())
예제 #6
0
    def save(self, *args, **kwargs):
        if not self.uuid:
            self.uuid = uuid4()
        if not self.pk and self.is_local:
            if not self.guid:
                self.guid = str(self.uuid)
            if not self.fid:
                self.fid = self.url

        if not self.fid and not self.handle:
            raise ValueError("Profile must have either a fid or a handle")

        if self.handle:
            # Ensure handle is *always* lowercase
            self.handle = self.handle.lower()
            if not validate_handle(self.handle):
                raise ValueError("Not a valid handle")

        # Set default pony images if image urls are empty
        if not self.image_url_small or not self.image_url_medium or not self.image_url_large:
            ponies = get_pony_urls()
            for idx, attr in enumerate(["image_url_large", "image_url_medium", "image_url_small"]):
                if not getattr(self, attr, None):
                    setattr(self, attr, ponies[idx])

        # Ensure keys are converted to str before saving
        self.rsa_private_key = decode_if_bytes(self.rsa_private_key)
        self.rsa_public_key = decode_if_bytes(self.rsa_public_key)

        super().save(*args, **kwargs)
예제 #7
0
def retrieve_remote_profile(id: str) -> Optional[Profile]:
    """High level retrieve profile method.

    Retrieve the profile from a remote location, using protocols based on the given ID.
    """
    if validate_handle(id):
        protocols = (activitypub_protocol, diaspora_protocol)
    else:
        protocols = (identify_protocol_by_id(id), )
    for protocol in protocols:
        utils = importlib.import_module(
            f"federation.utils.{protocol.PROTOCOL_NAME}")
        profile = utils.retrieve_and_parse_profile(id)
        if profile:
            return profile
예제 #8
0
 def save(self, *args, **kwargs):
     # Protect against empty guids which the search indexing would crash on
     if not self.guid:
         raise ValueError("Profile must have a guid!")
     if not validate_handle(self.handle):
         raise ValueError("Not a valid handle")
     # Set default pony images if image urls are empty
     if not self.image_url_small or not self.image_url_medium or not self.image_url_large:
         ponies = get_pony_urls()
         for idx, attr in enumerate(
             ["image_url_large", "image_url_medium", "image_url_small"]):
             if not getattr(self, attr, None):
                 setattr(self, attr, ponies[idx])
     # Ensure handle is *always* lowercase
     self.handle = self.handle.lower()
     super().save(*args, **kwargs)
예제 #9
0
    def get(self, request, *args, **kwargs):
        """See if we have a direct match. If so redirect, if not, search.

        Try fetching a remote profile if the search term is a handle or fid.
        """
        q = safe_text(request.GET.get("q"))
        if q:
            q = q.strip().lower()
        self.q = q
        # Check if direct tag matches
        if q.startswith('#'):
            try:
                tag = Tag.objects.filter(name=q[1:]).annotate(
                    content_count=Count('contents')).filter(
                        content_count__gt=0).get()
            except Tag.DoesNotExist:
                pass
            else:
                return redirect(tag.get_absolute_url())
        # Check if profile matches
        profile = None
        try:
            profile = Profile.objects.visible_for_user(
                request.user).fed(q).get()
        except Profile.DoesNotExist:
            # Try a remote search
            # TODO currently only if diaspora handle
            if validate_handle(q):
                try:
                    remote_profile = retrieve_remote_profile(q)
                except (AttributeError, ValueError,
                        xml.parsers.expat.ExpatError):
                    # Catch various errors parsing the remote profile
                    return super().get(request, *args, **kwargs)
                if remote_profile:
                    profile = Profile.from_remote_profile(remote_profile)
        if profile:
            return redirect(
                reverse("users:profile-detail", kwargs={"uuid": profile.uuid}))
        try:
            return super().get(request, *args, **kwargs)
        except QueryError:
            # Re-render the form
            messages.warning(
                self.request,
                _("Search string is invalid, please try another one."))
            return HttpResponseRedirect(self.get_success_url())
예제 #10
0
파일: views.py 프로젝트: jaywink/socialhome
    def get(self, request, *args, **kwargs):
        """See if we have a direct match. If so redirect, if not, search.

        Try fetching a remote profile if the search term is a handle or fid.
        """
        q = safe_text(request.GET.get("q"))
        if q:
            q = q.strip().lower()
        self.q = q
        # Check if direct tag matches
        if q.startswith('#'):
            try:
                tag = Tag.objects.filter(
                    name=q[1:]
                ).annotate(
                    content_count=Count('contents')
                ).filter(
                    content_count__gt=0
                ).get()
            except Tag.DoesNotExist:
                pass
            else:
                return redirect(tag.get_absolute_url())
        # Check if profile matches
        profile = None
        try:
            profile = Profile.objects.visible_for_user(request.user).fed(q).get()
        except Profile.DoesNotExist:
            # Try a remote search
            # TODO currently only if diaspora handle
            if validate_handle(q):
                try:
                    remote_profile = retrieve_remote_profile(q)
                except (AttributeError, ValueError, xml.parsers.expat.ExpatError):
                    # Catch various errors parsing the remote profile
                    return super().get(request, *args, **kwargs)
                if remote_profile:
                    profile = Profile.from_remote_profile(remote_profile)
        if profile:
            return redirect(reverse("users:profile-detail", kwargs={"uuid": profile.uuid}))
        try:
            return super().get(request, *args, **kwargs)
        except QueryError:
            # Re-render the form
            messages.warning(self.request, _("Search string is invalid, please try another one."))
            return HttpResponseRedirect(self.get_success_url())
예제 #11
0
    def save(self, *args, **kwargs):
        if not self.uuid:
            self.uuid = uuid4()
        if not self.pk and self.is_local:
            if not self.guid:
                self.guid = str(self.uuid)
            if not self.fid:
                self.fid = self.user.url
            # Default protocol for all new profiles
            self.protocol = "activitypub"

        if not self.fid and not self.handle:
            raise ValueError("Profile must have either a fid or a handle")

        if self.handle:
            # Ensure handle is *always* lowercase
            self.handle = self.handle.lower()
            if not validate_handle(self.handle):
                raise ValueError("Not a valid handle")
        else:
            self.handle = None

        if self.guid == "":
            self.guid = None

        # Set default pony images if image urls are empty
        if not self.image_url_small or not self.image_url_medium or not self.image_url_large:
            ponies = get_pony_urls()
            for idx, attr in enumerate(
                ["image_url_large", "image_url_medium", "image_url_small"]):
                if not getattr(self, attr, None):
                    setattr(self, attr, ponies[idx])

        # Ensure keys are converted to str before saving
        self.rsa_private_key = decode_if_bytes(self.rsa_private_key)
        self.rsa_public_key = decode_if_bytes(self.rsa_public_key)

        # Set default federation endpoints for local users
        if self.is_local:
            if not self.inbox_private:
                self.inbox_private = f"{self.fid}inbox/"
            if not self.inbox_public:
                self.inbox_public = f"{settings.SOCIALHOME_URL}{reverse('federate:receive-public')}"

        super().save(*args, **kwargs)
예제 #12
0
    def clean_recipients(self):
        """
        Check recipients can be found.
        """
        if self.cleaned_data.get('visibility') != Visibility.LIMITED or not self.cleaned_data.get('recipients'):
            return ""

        recipients = [r.strip() for r in self.cleaned_data.get('recipients', '').split(',')]

        for recipient in recipients:
            if not validate_handle(recipient):
                raise ValidationError(_("Recipient %s is not in the correct format." % recipient))
        recipient_profiles = Profile.objects.filter(handle__in=recipients).visible_for_user(self.user)
        # TODO we should probably try to lookup missing ones over the network first before failing
        if recipient_profiles.distinct().count() != len(set(recipients)):
            raise ValidationError(_("Not all recipients could be found."))

        return self.cleaned_data.get('recipients')
예제 #13
0
파일: forms.py 프로젝트: jaywink/socialhome
    def clean_recipients(self):
        """
        Check recipients can be found.
        """
        if self.cleaned_data.get('visibility') != Visibility.LIMITED or not self.cleaned_data.get('recipients'):
            return ""

        recipients = [r.strip() for r in self.cleaned_data.get('recipients', '').split(',')]

        for recipient in recipients:
            if not validate_handle(recipient) and not re.match(r"https?://", recipient):
                raise ValidationError(_("Recipient %s is not in the correct format." % recipient))
        recipient_profiles = Profile.objects.filter(
            Q(handle__in=recipients) | Q(fid__in=recipients)
        ).visible_for_user(self.user)
        # TODO we should probably try to lookup missing ones over the network first before failing
        if recipient_profiles.distinct().count() != len(set(recipients)):
            raise ValidationError(_("Not all recipients could be found."))

        return self.cleaned_data.get('recipients')
예제 #14
0
def retrieve_and_parse_profile(fid: str) -> Optional[ActivitypubProfile]:
    """
    Retrieve the remote fid and return a Profile object.
    """
    if validate_handle(fid):
        profile_id = get_profile_id_from_webfinger(fid)
        if not profile_id:
            return
    else:
        profile_id = fid
    profile = retrieve_and_parse_document(profile_id)
    if not profile:
        return
    try:
        profile.validate()
    except ValueError as ex:
        logger.warning(
            "retrieve_and_parse_profile - found profile %s but it didn't validate: %s",
            profile, ex)
        return
    return profile
예제 #15
0
    def validate_recipients(self, value: Set[str]) -> Set[str]:
        if self.initial_data.get(
                "visibility",
                Visibility.PUBLIC.value) != Visibility.LIMITED.value:
            return value

        if not value and not self.initial_data.get("include_following"):
            raise serializers.ValidationError(
                "Recipients cannot be empty if not including followed users.")

        user = self.context.get("request").user

        validation_errors = []
        for recipient in value:
            if not validate_handle(recipient) and not re.match(
                    r"https?://", recipient):
                validation_errors.append(recipient)

        if len(validation_errors) > 0:
            msg = _(
                "This recipient couldn't be found (please check the format).",
                "These recipients couldn't be found (please check the format).",
                len(validation_errors))

            raise serializers.ValidationError({
                "code": "recipients_not_found_error",
                "message": msg,
                "payload": validation_errors,
            })

        recipient_profiles = Profile.objects.filter(
            Q(handle__in=value) | Q(fid__in=value)).visible_for_user(user)

        # TODO we should probably try to lookup missing ones over the network first before failing
        if recipient_profiles.distinct().count() != len(set(value)):
            raise serializers.ValidationError(
                "Not all recipients could be found.")

        return value
예제 #16
0
    def to_as2(self) -> Dict:
        as2 = {
            "@context":
            CONTEXTS_DEFAULT + [
                CONTEXT_HASHTAG,
                CONTEXT_LD_SIGNATURES,
                CONTEXT_SENSITIVE,
            ],
            "type":
            self.activity.value,
            "id":
            self.activity_id,
            "actor":
            self.actor_id,
            "object": {
                "id": self.id,
                "type": self._type,
                "attributedTo": self.actor_id,
                "content": self.rendered_content,
                "published": self.created_at.isoformat(),
                "inReplyTo": None,
                "sensitive": True if "nsfw" in self.tags else False,
                "summary":
                None,  # TODO Short text? First sentence? First line?
                "url": self.url,
                'source': {
                    'content': self.raw_content,
                    'mediaType': self._media_type,
                },
                "tag": [],
            },
            "published":
            self.created_at.isoformat(),
        }

        if len(self._children):
            as2["object"]["attachment"] = []
            for child in self._children:
                as2["object"]["attachment"].append(child.to_as2())

        if len(self._mentions):
            mentions = list(self._mentions)
            mentions.sort()
            for mention in mentions:
                if mention.startswith("http"):
                    as2["object"]["tag"].append({
                        'type': 'Mention',
                        'href': mention,
                        'name': mention,
                    })
                elif validate_handle(mention):
                    # Look up via WebFinger
                    as2["object"]["tag"].append({
                        'type': 'Mention',
                        'href':
                        mention,  # TODO need to implement fetch via webfinger for AP handles first
                        'name': mention,
                    })

        as2["object"]["tag"].extend(self.add_object_tags())
        return as2
예제 #17
0
 def validate_target_handle(self):
     if not validate_handle(self.target_handle):
         raise ValueError("Target handle is not valid")
예제 #18
0
 def validate_handle(self):
     if not validate_handle(self.handle):
         raise ValueError("Handle is not valid")
예제 #19
0
def identify_recipient_protocol(id: str) -> Optional[str]:
    if re.match(r'^https?://', id, flags=re.IGNORECASE) is not None:
        return "activitypub"
    if validate_handle(id):
        return "diaspora"
예제 #20
0
def identify_id(id: str) -> bool:
    """
    Try to identify if this ID is a Diaspora ID.
    """
    return validate_handle(id)