From 3ccb70d0a827060e79ef3a84a5d9cbe884b1b412 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Thu, 29 Jun 2017 02:27:35 +0200
Subject: [PATCH] Fixed #15 again, now check authorization also using query
param
---
.env.dev | 4 +--
api/config/settings/common.py | 1 +
api/funkwhale_api/common/authentication.py | 20 ++++++++++++
.../common/tests/test_jwt_querystring.py | 32 +++++++++++++++++++
api/funkwhale_api/music/views.py | 8 +++--
front/src/audio/queue.js | 14 +++++++-
front/src/auth/index.js | 8 +++--
front/src/components/browse/Track.vue | 8 ++++-
front/src/utils/url.js | 11 +++++++
9 files changed, 98 insertions(+), 8 deletions(-)
create mode 100644 api/funkwhale_api/common/authentication.py
create mode 100644 api/funkwhale_api/common/tests/test_jwt_querystring.py
create mode 100644 front/src/utils/url.js
diff --git a/.env.dev b/.env.dev
index de58e2758..a7413b0ff 100644
--- a/.env.dev
+++ b/.env.dev
@@ -1,3 +1,3 @@
-BACKEND_URL=http://localhost:12081
+BACKEND_URL=http://localhost:6001
YOUTUBE_API_KEY=
-API_AUTHENTICATION_REQUIRED=False
+API_AUTHENTICATION_REQUIRED=True
diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index 5ba197145..b6e195ca2 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -288,6 +288,7 @@ REST_FRAMEWORK = {
'PAGE_SIZE': 25,
'DEFAULT_AUTHENTICATION_CLASSES': (
+ 'funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS',
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
diff --git a/api/funkwhale_api/common/authentication.py b/api/funkwhale_api/common/authentication.py
new file mode 100644
index 000000000..b75f3b516
--- /dev/null
+++ b/api/funkwhale_api/common/authentication.py
@@ -0,0 +1,20 @@
+from rest_framework import exceptions
+from rest_framework_jwt import authentication
+from rest_framework_jwt.settings import api_settings
+
+
+class JSONWebTokenAuthenticationQS(
+ authentication.BaseJSONWebTokenAuthentication):
+
+ www_authenticate_realm = 'api'
+
+ def get_jwt_value(self, request):
+ token = request.query_params.get('jwt')
+ if 'jwt' in request.query_params and not token:
+ msg = _('Invalid Authorization header. No credentials provided.')
+ raise exceptions.AuthenticationFailed(msg)
+ return token
+
+ def authenticate_header(self, request):
+ return '{0} realm="{1}"'.format(
+ api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm)
diff --git a/api/funkwhale_api/common/tests/test_jwt_querystring.py b/api/funkwhale_api/common/tests/test_jwt_querystring.py
new file mode 100644
index 000000000..90e63775d
--- /dev/null
+++ b/api/funkwhale_api/common/tests/test_jwt_querystring.py
@@ -0,0 +1,32 @@
+from test_plus.test import TestCase
+from rest_framework_jwt.settings import api_settings
+
+from funkwhale_api.users.models import User
+
+
+jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
+jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
+
+
+class TestJWTQueryString(TestCase):
+ www_authenticate_realm = 'api'
+
+ def test_can_authenticate_using_token_param_in_url(self):
+ user = User.objects.create_superuser(
+ username='test', email='test@test.com', password='test')
+
+ url = self.reverse('api:v1:tracks-list')
+ with self.settings(API_AUTHENTICATION_REQUIRED=True):
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 401)
+
+ payload = jwt_payload_handler(user)
+ token = jwt_encode_handler(payload)
+ print(payload, token)
+ with self.settings(API_AUTHENTICATION_REQUIRED=True):
+ response = self.client.get(url, data={
+ 'jwt': token
+ })
+
+ self.assertEqual(response.status_code, 200)
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index 506db1239..4a4032c57 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -1,5 +1,7 @@
import os
import json
+import unicodedata
+import urllib
from django.core.urlresolvers import reverse
from django.db import models, transaction
from django.db.models.functions import Length
@@ -137,8 +139,10 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
return Response(status=404)
response = Response()
- response["Content-Disposition"] = "attachment; filename={0}".format(
- f.audio_file.name)
+ filename = "filename*=UTF-8''{}{}".format(
+ urllib.parse.quote(f.track.full_name),
+ os.path.splitext(f.audio_file.name)[-1])
+ response["Content-Disposition"] = "attachment; {}".format(filename)
response['X-Accel-Redirect'] = "{}{}".format(
settings.PROTECT_FILES_PATH,
f.audio_file.url)
diff --git a/front/src/audio/queue.js b/front/src/audio/queue.js
index c91c1d2ac..efa3dcdf7 100644
--- a/front/src/audio/queue.js
+++ b/front/src/audio/queue.js
@@ -5,6 +5,8 @@ import Audio from '@/audio'
import backend from '@/audio/backend'
import radios from '@/radios'
import Vue from 'vue'
+import url from '@/utils/url'
+import auth from '@/auth'
class Queue {
constructor (options = {}) {
@@ -181,7 +183,17 @@ class Queue {
if (!file) {
return this.next()
}
- this.audio = new Audio(backend.absoluteUrl(file.path), {
+ let path = backend.absoluteUrl(file.path)
+
+ if (auth.user.authenticated) {
+ // we need to send the token directly in url
+ // so authentication can be checked by the backend
+ // because for audio files we cannot use the regular Authentication
+ // header
+ path = url.updateQueryString(path, 'jwt', auth.getAuthToken())
+ }
+
+ this.audio = new Audio(path, {
preload: true,
autoplay: true,
rate: 1,
diff --git a/front/src/auth/index.js b/front/src/auth/index.js
index b5a3fb5ad..219a1531f 100644
--- a/front/src/auth/index.js
+++ b/front/src/auth/index.js
@@ -50,7 +50,7 @@ export default {
checkAuth () {
logger.default.info('Checking authentication...')
- var jwt = cache.get('token')
+ var jwt = this.getAuthToken()
var username = cache.get('username')
if (jwt) {
this.user.authenticated = true
@@ -63,9 +63,13 @@ export default {
}
},
+ getAuthToken () {
+ return cache.get('token')
+ },
+
// The object to be passed as a header for authenticated requests
getAuthHeader () {
- return 'JWT ' + cache.get('token')
+ return 'JWT ' + this.getAuthToken()
},
fetchProfile () {
diff --git a/front/src/components/browse/Track.vue b/front/src/components/browse/Track.vue
index 336af285b..1e1568793 100644
--- a/front/src/components/browse/Track.vue
+++ b/front/src/components/browse/Track.vue
@@ -61,6 +61,8 @@
<script>
+import auth from '@/auth'
+import url from '@/utils/url'
import logger from '@/logging'
import backend from '@/audio/backend'
import PlayButton from '@/components/audio/PlayButton'
@@ -121,7 +123,11 @@ export default {
},
downloadUrl () {
if (this.track.files.length > 0) {
- return backend.absoluteUrl(this.track.files[0].path)
+ let u = backend.absoluteUrl(this.track.files[0].path)
+ if (auth.user.authenticated) {
+ u = url.updateQueryString(u, 'jwt', auth.getAuthToken())
+ }
+ return u
}
},
lyricsSearchUrl () {
diff --git a/front/src/utils/url.js b/front/src/utils/url.js
new file mode 100644
index 000000000..61a430988
--- /dev/null
+++ b/front/src/utils/url.js
@@ -0,0 +1,11 @@
+export default {
+ updateQueryString (uri, key, value) {
+ var re = new RegExp('([?&])' + key + '=.*?(&|$)', 'i')
+ var separator = uri.indexOf('?') !== -1 ? '&' : '?'
+ if (uri.match(re)) {
+ return uri.replace(re, '$1' + key + '=' + value + '$2')
+ } else {
+ return uri + separator + key + '=' + value
+ }
+ }
+}
--
GitLab