From 9612b1bace4b59cafeed1304971395ffbc1ececb Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 7 Apr 2018 15:34:35 +0200 Subject: [PATCH] Can now serve track from remote library --- api/funkwhale_api/federation/activity.py | 15 +--- api/funkwhale_api/federation/actors.py | 32 ++++++--- .../federation/authentication.py | 2 + .../federation/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/generate_keys.py | 53 -------------- ...407_0852.py => 0003_auto_20180407_1010.py} | 4 +- api/funkwhale_api/federation/models.py | 7 -- api/funkwhale_api/federation/signing.py | 15 ++++ api/funkwhale_api/music/factories.py | 12 ++++ ...407_0852.py => 0023_auto_20180407_1010.py} | 9 ++- api/funkwhale_api/music/models.py | 14 ++-- api/funkwhale_api/music/permissions.py | 23 +++++++ api/funkwhale_api/music/tasks.py | 5 +- api/funkwhale_api/music/utils.py | 7 ++ api/funkwhale_api/music/views.py | 69 +++++++++++++++---- api/tests/conftest.py | 9 +++ api/tests/federation/conftest.py | 10 --- api/tests/federation/test_actors.py | 16 +++-- api/tests/federation/test_commands.py | 14 ---- api/tests/music/test_permissions.py | 56 +++++++++++++++ api/tests/music/test_views.py | 40 +++++++++++ 22 files changed, 272 insertions(+), 140 deletions(-) delete mode 100644 api/funkwhale_api/federation/management/__init__.py delete mode 100644 api/funkwhale_api/federation/management/commands/__init__.py delete mode 100644 api/funkwhale_api/federation/management/commands/generate_keys.py rename api/funkwhale_api/federation/migrations/{0003_auto_20180407_0852.py => 0003_auto_20180407_1010.py} (94%) rename api/funkwhale_api/music/migrations/{0023_auto_20180407_0852.py => 0023_auto_20180407_1010.py} (88%) create mode 100644 api/funkwhale_api/music/permissions.py delete mode 100644 api/tests/federation/conftest.py delete mode 100644 api/tests/federation/test_commands.py create mode 100644 api/tests/music/test_permissions.py diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py index db71bd4fb..7502bd739 100644 --- a/api/funkwhale_api/federation/activity.py +++ b/api/funkwhale_api/federation/activity.py @@ -58,21 +58,12 @@ OBJECT_TYPES = [ 'Video', ] + ACTIVITY_TYPES + def deliver(activity, on_behalf_of, to=[]): from . import actors logger.info('Preparing activity delivery to %s', to) - auth = requests_http_signature.HTTPSignatureAuth( - use_auth_header=False, - headers=[ - '(request-target)', - 'user-agent', - 'host', - 'date', - 'content-type',], - algorithm='rsa-sha256', - key=on_behalf_of.private_key.encode('utf-8'), - key_id=on_behalf_of.private_key_id, - ) + auth = signing.get_auth( + on_behalf_of.private_key, on_behalf_of.private_key_id) for url in to: recipient_actor = actors.get_actor(url) logger.debug('delivering to %s', recipient_actor.inbox_url) diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index ffbafd8b7..f640c9b12 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -13,8 +13,10 @@ from rest_framework.exceptions import PermissionDenied from dynamic_preferences.registries import global_preferences_registry from . import activity +from . import keys from . import models from . import serializers +from . import signing from . import utils logger = logging.getLogger(__name__) @@ -51,24 +53,37 @@ class SystemActor(object): additional_attributes = {} manually_approves_followers = False + def get_request_auth(self): + actor = self.get_actor_instance() + return signing.get_auth( + actor.private_key, actor.private_key_id) + def serialize(self): actor = self.get_actor_instance() serializer = serializers.ActorSerializer(actor) return serializer.data def get_actor_instance(self): + try: + return models.Actor.objects.get(url=self.get_actor_url()) + except models.Actor.DoesNotExist: + pass + private, public = keys.get_key_pair() args = self.get_instance_argument( self.id, name=self.name, summary=self.summary, **self.additional_attributes ) - url = args.pop('url') - a, created = models.Actor.objects.get_or_create( - url=url, - defaults=args, - ) - return a + args['private_key'] = private.decode('utf-8') + args['public_key'] = public.decode('utf-8') + return models.Actor.objects.create(**args) + + def get_actor_url(self): + return utils.full_url( + reverse( + 'federation:instance-actors-detail', + kwargs={'actor': self.id})) def get_instance_argument(self, id, name, summary, **kwargs): preferences = global_preferences_registry.manager() @@ -78,10 +93,7 @@ class SystemActor(object): 'type': 'Person', 'name': name.format(host=settings.FEDERATION_HOSTNAME), 'manually_approves_followers': True, - 'url': utils.full_url( - reverse( - 'federation:instance-actors-detail', - kwargs={'actor': id})), + 'url': self.get_actor_url(), 'shared_inbox_url': utils.full_url( reverse( 'federation:instance-actors-inbox', diff --git a/api/funkwhale_api/federation/authentication.py b/api/funkwhale_api/federation/authentication.py index f2926bb30..7f8ad6653 100644 --- a/api/funkwhale_api/federation/authentication.py +++ b/api/funkwhale_api/federation/authentication.py @@ -51,6 +51,8 @@ class SignatureAuthentication(authentication.BaseAuthentication): def authenticate(self, request): setattr(request, 'actor', None) actor = self.authenticate_actor(request) + if not actor: + return user = AnonymousUser() setattr(request, 'actor', actor) return (user, None) diff --git a/api/funkwhale_api/federation/management/__init__.py b/api/funkwhale_api/federation/management/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/api/funkwhale_api/federation/management/commands/__init__.py b/api/funkwhale_api/federation/management/commands/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/api/funkwhale_api/federation/management/commands/generate_keys.py b/api/funkwhale_api/federation/management/commands/generate_keys.py deleted file mode 100644 index eafe9aae3..000000000 --- a/api/funkwhale_api/federation/management/commands/generate_keys.py +++ /dev/null @@ -1,53 +0,0 @@ -from django.core.management.base import BaseCommand, CommandError -from django.db import transaction - -from dynamic_preferences.registries import global_preferences_registry - -from funkwhale_api.federation import keys - - -class Command(BaseCommand): - help = ( - 'Generate a public/private key pair for your instance,' - ' for federation purposes. If a key pair already exists, does nothing.' - ) - - def add_arguments(self, parser): - parser.add_argument( - '--replace', - action='store_true', - dest='replace', - default=False, - help='Replace existing key pair, if any', - ) - parser.add_argument( - '--noinput', '--no-input', action='store_false', dest='interactive', - help="Do NOT prompt the user for input of any kind.", - ) - - @transaction.atomic - def handle(self, *args, **options): - preferences = global_preferences_registry.manager() - existing_public = preferences['federation__public_key'] - existing_private = preferences['federation__public_key'] - - if existing_public or existing_private and not options['replace']: - raise CommandError( - 'Keys are already present! ' - 'Replace them with --replace if you know what you are doing.') - - if options['interactive']: - message = ( - 'Are you sure you want to do this?\n\n' - "Type 'yes' to continue, or 'no' to cancel: " - ) - if input(''.join(message)) != 'yes': - raise CommandError("Operation cancelled.") - private, public = keys.get_key_pair() - preferences['federation__public_key'] = public.decode('utf-8') - preferences['federation__private_key'] = private.decode('utf-8') - - self.stdout.write( - 'Your new key pair was generated.' - 'Your public key is now:\n\n{}'.format(public.decode('utf-8')) - ) diff --git a/api/funkwhale_api/federation/migrations/0003_auto_20180407_0852.py b/api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py similarity index 94% rename from api/funkwhale_api/federation/migrations/0003_auto_20180407_0852.py rename to api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py index afc7ea083..38ac7cb4f 100644 --- a/api/funkwhale_api/federation/migrations/0003_auto_20180407_0852.py +++ b/api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.3 on 2018-04-07 08:52 +# Generated by Django 2.0.3 on 2018-04-07 10:10 import django.contrib.postgres.fields.jsonb from django.db import migrations, models @@ -10,7 +10,6 @@ import uuid class Migration(migrations.Migration): dependencies = [ - ('music', '0022_importbatch_import_request'), ('federation', '0002_auto_20180403_1620'), ] @@ -70,7 +69,6 @@ class Migration(migrations.Migration): ('title', models.CharField(max_length=500)), ('metadata', django.contrib.postgres.fields.jsonb.JSONField(default={}, max_length=10000)), ('library', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tracks', to='federation.Library')), - ('local_track_file', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='library_track', to='music.TrackFile')), ], ), migrations.AddField( diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index 91f2ea973..bf1e5d830 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -192,13 +192,6 @@ class LibraryTrack(models.Model): published_date = models.DateTimeField(null=True, blank=True) library = models.ForeignKey( Library, related_name='tracks', on_delete=models.CASCADE) - local_track_file = models.OneToOneField( - 'music.TrackFile', - related_name='library_track', - on_delete=models.CASCADE, - null=True, - blank=True, - ) artist_name = models.CharField(max_length=500) album_title = models.CharField(max_length=500) title = models.CharField(max_length=500) diff --git a/api/funkwhale_api/federation/signing.py b/api/funkwhale_api/federation/signing.py index 7e4d2aa5a..8d984d3ff 100644 --- a/api/funkwhale_api/federation/signing.py +++ b/api/funkwhale_api/federation/signing.py @@ -53,3 +53,18 @@ def verify_django(django_request, public_key): request.headers[h] = str(v) prepared_request = request.prepare() return verify(request, public_key) + + +def get_auth(private_key, private_key_id): + return requests_http_signature.HTTPSignatureAuth( + use_auth_header=False, + headers=[ + '(request-target)', + 'user-agent', + 'host', + 'date', + 'content-type'], + algorithm='rsa-sha256', + key=private_key.encode('utf-8'), + key_id=private_key_id, + ) diff --git a/api/funkwhale_api/music/factories.py b/api/funkwhale_api/music/factories.py index 83aad432a..2bf1960ca 100644 --- a/api/funkwhale_api/music/factories.py +++ b/api/funkwhale_api/music/factories.py @@ -56,6 +56,18 @@ class TrackFileFactory(factory.django.DjangoModelFactory): class Meta: model = 'music.TrackFile' + class Params: + federation = factory.Trait( + audio_file=None, + library_track=factory.SubFactory(LibraryTrackFactory), + mimetype=factory.LazyAttribute( + lambda o: o.library_track.audio_mimetype + ), + source=factory.LazyAttribute( + lambda o: o.library_track.audio_url + ), + ) + @registry.register class ImportBatchFactory(factory.django.DjangoModelFactory): diff --git a/api/funkwhale_api/music/migrations/0023_auto_20180407_0852.py b/api/funkwhale_api/music/migrations/0023_auto_20180407_1010.py similarity index 88% rename from api/funkwhale_api/music/migrations/0023_auto_20180407_0852.py rename to api/funkwhale_api/music/migrations/0023_auto_20180407_1010.py index b1bbeacef..0539d90f6 100644 --- a/api/funkwhale_api/music/migrations/0023_auto_20180407_0852.py +++ b/api/funkwhale_api/music/migrations/0023_auto_20180407_1010.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.3 on 2018-04-07 08:52 +# Generated by Django 2.0.3 on 2018-04-07 10:10 from django.conf import settings from django.db import migrations, models @@ -10,7 +10,7 @@ import uuid class Migration(migrations.Migration): dependencies = [ - ('federation', '0003_auto_20180407_0852'), + ('federation', '0003_auto_20180407_1010'), ('music', '0022_importbatch_import_request'), ] @@ -55,6 +55,11 @@ class Migration(migrations.Migration): name='creation_date', field=models.DateTimeField(default=django.utils.timezone.now), ), + migrations.AddField( + model_name='trackfile', + name='library_track', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='local_track_file', to='federation.LibraryTrack'), + ), migrations.AddField( model_name='trackfile', name='modification_date', diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index bcf691bcd..beec551a5 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -419,6 +419,14 @@ class TrackFile(models.Model): acoustid_track_id = models.UUIDField(null=True, blank=True) mimetype = models.CharField(null=True, blank=True, max_length=200) + library_track = models.OneToOneField( + 'federation.LibraryTrack', + related_name='local_track_file', + on_delete=models.CASCADE, + null=True, + blank=True, + ) + def download_file(self): # import the track file, since there is not any # we create a tmp dir for the download @@ -441,10 +449,8 @@ class TrackFile(models.Model): @property def path(self): - if settings.PROTECT_AUDIO_FILES: - return reverse( - 'api:v1:trackfiles-serve', kwargs={'pk': self.pk}) - return self.audio_file.url + return reverse( + 'api:v1:trackfiles-serve', kwargs={'pk': self.pk}) @property def filename(self): diff --git a/api/funkwhale_api/music/permissions.py b/api/funkwhale_api/music/permissions.py new file mode 100644 index 000000000..a8e62f1e7 --- /dev/null +++ b/api/funkwhale_api/music/permissions.py @@ -0,0 +1,23 @@ +from django.conf import settings + +from rest_framework.permissions import BasePermission + +from funkwhale_api.federation import actors + + +class Listen(BasePermission): + + def has_permission(self, request, view): + if not settings.PROTECT_AUDIO_FILES: + return True + + user = getattr(request, 'user', None) + if user and user.is_authenticated: + return True + + actor = getattr(request, 'actor', None) + if actor is None: + return False + + library = actors.SYSTEM_ACTORS['library'].get_actor_instance() + return library.followers.filter(url=actor.url).exists() diff --git a/api/funkwhale_api/music/tasks.py b/api/funkwhale_api/music/tasks.py index c58eb7136..012b72cd2 100644 --- a/api/funkwhale_api/music/tasks.py +++ b/api/funkwhale_api/music/tasks.py @@ -112,6 +112,7 @@ def _do_import(import_job, replace, use_acoustid=True): track_file.audio_file.name = import_job.audio_file.name track_file.duration = duration elif import_job.library_track: + track_file.library_track = import_job.library_track track_file.mimetype = import_job.library_track.audio_mimetype if import_job.library_track.library.download_files: raise NotImplementedError() @@ -121,10 +122,6 @@ def _do_import(import_job, replace, use_acoustid=True): else: track_file.download_file() track_file.save() - if import_job.library_track: - import_job.library_track.local_track_file = track_file - import_job.library_track.save( - update_fields=['modification_date', 'local_track_file']) import_job.status = 'finished' import_job.track_file = track_file if import_job.audio_file: diff --git a/api/funkwhale_api/music/utils.py b/api/funkwhale_api/music/utils.py index df659cb80..af0e59ab4 100644 --- a/api/funkwhale_api/music/utils.py +++ b/api/funkwhale_api/music/utils.py @@ -60,3 +60,10 @@ def compute_status(jobs): if pending: return 'pending' return 'finished' + + +def get_ext_from_type(mimetype): + mapping = { + 'audio/ogg': 'ogg', + 'audio/mpeg': 'mp3', + } diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 5ac3143f9..5f8fc1736 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -1,36 +1,42 @@ import ffmpeg import os import json +import requests import subprocess import unicodedata import urllib -from django.urls import reverse +from django.contrib.auth.decorators import login_required +from django.core.exceptions import ObjectDoesNotExist +from django.conf import settings from django.db import models, transaction from django.db.models.functions import Length -from django.conf import settings from django.http import StreamingHttpResponse +from django.urls import reverse +from django.utils.decorators import method_decorator from rest_framework import viewsets, views, mixins from rest_framework.decorators import detail_route, list_route from rest_framework.response import Response +from rest_framework import settings as rest_settings from rest_framework import permissions from musicbrainzngs import ResponseError -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator from funkwhale_api.common import utils as funkwhale_utils +from funkwhale_api.federation import actors from funkwhale_api.requests.models import ImportRequest from funkwhale_api.musicbrainz import api from funkwhale_api.common.permissions import ( ConditionalAuthentication, HasModelPermission) from taggit.models import Tag +from funkwhale_api.federation.authentication import SignatureAuthentication -from . import forms -from . import models -from . import serializers -from . import importers from . import filters +from . import forms +from . import importers +from . import models +from . import permissions as music_permissions +from . import serializers from . import tasks from . import utils @@ -45,6 +51,7 @@ class SearchMixin(object): serializer = self.serializer_class(queryset, many=True) return Response(serializer.data) + class TagViewSetMixin(object): def get_queryset(self): @@ -179,22 +186,54 @@ class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet): class TrackFileViewSet(viewsets.ReadOnlyModelViewSet): queryset = (models.TrackFile.objects.all().order_by('-id')) serializer_class = serializers.TrackFileSerializer - permission_classes = [ConditionalAuthentication] + authentication_classes = rest_settings.api_settings.DEFAULT_AUTHENTICATION_CLASSES + [ + SignatureAuthentication + ] + permission_classes = [music_permissions.Listen] @detail_route(methods=['get']) def serve(self, request, *args, **kwargs): try: - f = models.TrackFile.objects.get(pk=kwargs['pk']) + f = models.TrackFile.objects.select_related( + 'library_track', + 'track__album__artist', + 'track__artist', + ).get(pk=kwargs['pk']) except models.TrackFile.DoesNotExist: return Response(status=404) - response = Response() + mt = f.mimetype + try: + library_track = f.library_track + except ObjectDoesNotExist: + library_track = None + if library_track and not f.audio_file: + # we proxy the response to the remote library + # since we did not mirror the file locally + mt = library_track.audio_mimetype + file_extension = utils.get_ext_from_type(mt) + filename = '{}.{}'.format(f.track.full_name, file_extension) + auth = actors.SYSTEM_ACTORS['library'].get_request_auth() + remote_response = requests.get( + library_track.audio_url, + auth=auth, + stream=True, + headers={ + 'Content-Type': 'application/activity+json' + }) + response = StreamingHttpResponse(remote_response.iter_content()) + else: + response = Response() + filename = f.filename + response['X-Accel-Redirect'] = "{}{}".format( + settings.PROTECT_FILES_PATH, + f.audio_file.url) filename = "filename*=UTF-8''{}".format( - urllib.parse.quote(f.filename)) + urllib.parse.quote(filename)) response["Content-Disposition"] = "attachment; {}".format(filename) - response['X-Accel-Redirect'] = "{}{}".format( - settings.PROTECT_FILES_PATH, - f.audio_file.url) + if mt: + response["Content-Type"] = mt + return response @list_route(methods=['get']) diff --git a/api/tests/conftest.py b/api/tests/conftest.py index d5bb56565..4f1ee8962 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -162,3 +162,12 @@ def media_root(settings): def r_mock(): with requests_mock.mock() as m: yield m + + +@pytest.fixture +def authenticated_actor(factories, mocker): + actor = factories['federation.Actor']() + mocker.patch( + 'funkwhale_api.federation.authentication.SignatureAuthentication.authenticate_actor', + return_value=actor) + yield actor diff --git a/api/tests/federation/conftest.py b/api/tests/federation/conftest.py deleted file mode 100644 index c5831914b..000000000 --- a/api/tests/federation/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -import pytest - - -@pytest.fixture -def authenticated_actor(nodb_factories, mocker): - actor = nodb_factories['federation.Actor']() - mocker.patch( - 'funkwhale_api.federation.authentication.SignatureAuthentication.authenticate_actor', - return_value=actor) - yield actor diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py index 107047b56..090d9b03f 100644 --- a/api/tests/federation/test_actors.py +++ b/api/tests/federation/test_actors.py @@ -26,14 +26,17 @@ def test_actor_fetching(r_mock): assert r == payload -def test_get_library(settings, preferences): - preferences['federation__public_key'] = 'public_key' +def test_get_library(db, settings, mocker): + get_key_pair = mocker.patch( + 'funkwhale_api.federation.keys.get_key_pair', + return_value=(b'private', b'public')) expected = { 'preferred_username': 'library', 'domain': settings.FEDERATION_HOSTNAME, 'type': 'Person', 'name': '{}\'s library'.format(settings.FEDERATION_HOSTNAME), 'manually_approves_followers': True, + 'public_key': 'public', 'url': utils.full_url( reverse( 'federation:instance-actors-detail', @@ -50,7 +53,6 @@ def test_get_library(settings, preferences): reverse( 'federation:instance-actors-outbox', kwargs={'actor': 'library'})), - 'public_key': 'public_key', 'summary': 'Bot account to federate with {}\'s library'.format( settings.FEDERATION_HOSTNAME), } @@ -59,14 +61,17 @@ def test_get_library(settings, preferences): assert getattr(actor, key) == value -def test_get_test(settings, preferences): - preferences['federation__public_key'] = 'public_key' +def test_get_test(db, mocker, settings): + get_key_pair = mocker.patch( + 'funkwhale_api.federation.keys.get_key_pair', + return_value=(b'private', b'public')) expected = { 'preferred_username': 'test', 'domain': settings.FEDERATION_HOSTNAME, 'type': 'Person', 'name': '{}\'s test account'.format(settings.FEDERATION_HOSTNAME), 'manually_approves_followers': False, + 'public_key': 'public', 'url': utils.full_url( reverse( 'federation:instance-actors-detail', @@ -83,7 +88,6 @@ def test_get_test(settings, preferences): reverse( 'federation:instance-actors-outbox', kwargs={'actor': 'test'})), - 'public_key': 'public_key', 'summary': 'Bot account to test federation with {}. Send me /ping and I\'ll answer you.'.format( settings.FEDERATION_HOSTNAME), } diff --git a/api/tests/federation/test_commands.py b/api/tests/federation/test_commands.py deleted file mode 100644 index 7c5333068..000000000 --- a/api/tests/federation/test_commands.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.core.management import call_command - - -def test_generate_instance_key_pair(preferences, mocker): - mocker.patch( - 'funkwhale_api.federation.keys.get_key_pair', - return_value=(b'private', b'public')) - assert preferences['federation__public_key'] == '' - assert preferences['federation__private_key'] == '' - - call_command('generate_keys', interactive=False) - - assert preferences['federation__private_key'] == 'private' - assert preferences['federation__public_key'] == 'public' diff --git a/api/tests/music/test_permissions.py b/api/tests/music/test_permissions.py new file mode 100644 index 000000000..6cce85e08 --- /dev/null +++ b/api/tests/music/test_permissions.py @@ -0,0 +1,56 @@ +from rest_framework.views import APIView + +from funkwhale_api.federation import actors +from funkwhale_api.music import permissions + + +def test_list_permission_no_protect(anonymous_user, api_request, settings): + settings.PROTECT_AUDIO_FILES = False + view = APIView.as_view() + permission = permissions.Listen() + request = api_request.get('/') + assert permission.has_permission(request, view) is True + + +def test_list_permission_protect_anonymous( + anonymous_user, api_request, settings): + settings.PROTECT_AUDIO_FILES = True + view = APIView.as_view() + permission = permissions.Listen() + request = api_request.get('/') + assert permission.has_permission(request, view) is False + + +def test_list_permission_protect_authenticated( + factories, api_request, settings): + settings.PROTECT_AUDIO_FILES = True + user = factories['users.User']() + view = APIView.as_view() + permission = permissions.Listen() + request = api_request.get('/') + setattr(request, 'user', user) + assert permission.has_permission(request, view) is True + + +def test_list_permission_protect_not_following_actor( + factories, api_request, settings): + settings.PROTECT_AUDIO_FILES = True + actor = factories['federation.Actor']() + view = APIView.as_view() + permission = permissions.Listen() + request = api_request.get('/') + setattr(request, 'actor', actor) + assert permission.has_permission(request, view) is False + + +def test_list_permission_protect_following_actor( + factories, api_request, settings): + settings.PROTECT_AUDIO_FILES = True + library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + follow = factories['federation.Follow'](target=library_actor) + view = APIView.as_view() + permission = permissions.Listen() + request = api_request.get('/') + setattr(request, 'actor', follow.actor) + + assert permission.has_permission(request, view) is True diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py index 295604616..468ea77e3 100644 --- a/api/tests/music/test_views.py +++ b/api/tests/music/test_views.py @@ -1,6 +1,8 @@ +import io import pytest from funkwhale_api.music import views +from funkwhale_api.federation import actors @pytest.mark.parametrize('param,expected', [ @@ -43,3 +45,41 @@ def test_album_view_filter_listenable( queryset = view.filter_queryset(view.get_queryset()) assert list(queryset) == expected + + +def test_can_serve_track_file_as_remote_library( + factories, authenticated_actor, settings, api_client): + settings.PROTECT_AUDIO_FILES = True + library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + follow = factories['federation.Follow']( + actor=authenticated_actor, target=library_actor) + + track_file = factories['music.TrackFile']() + response = api_client.get(track_file.path) + + assert response.status_code == 200 + assert response['X-Accel-Redirect'] == "{}{}".format( + settings.PROTECT_FILES_PATH, + track_file.audio_file.url) + + +def test_can_serve_track_file_as_remote_library_deny_not_following( + factories, authenticated_actor, settings, api_client): + settings.PROTECT_AUDIO_FILES = True + track_file = factories['music.TrackFile']() + response = api_client.get(track_file.path) + + assert response.status_code == 403 + + +def test_can_proxy_remote_track( + factories, settings, api_client, r_mock): + settings.PROTECT_AUDIO_FILES = False + track_file = factories['music.TrackFile'](federation=True) + + r_mock.get(track_file.library_track.audio_url, body=io.StringIO('test')) + response = api_client.get(track_file.path) + + assert response.status_code == 200 + assert list(response.streaming_content) == [b't', b'e', b's', b't'] + assert response['Content-Type'] == track_file.library_track.audio_mimetype