Skip to content

Commit

Permalink
Fixed event loop closed error, removed file download endpoint, implem…
Browse files Browse the repository at this point in the history
…ented nginx subrequest support
  • Loading branch information
RineshRamadhin committed Nov 19, 2022
1 parent f21cbce commit 9520908
Show file tree
Hide file tree
Showing 15 changed files with 106 additions and 132 deletions.
8 changes: 4 additions & 4 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@


# Build information of the Web DL API
VERSION = '2.26.1'
VERSION = '3.0.0'
REPOSITORY = 'https://github.com/web-dl-tools/api.git'

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
Expand Down Expand Up @@ -65,7 +65,7 @@
"src.db",
"src.user",
"src.application",
"src.auth_token",
"src.authentication",
"src.download",
"src.handlers",
"src.handlers.audio_visual",
Expand Down Expand Up @@ -194,8 +194,8 @@
}
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {"hosts": [("redis", 6379)]},
"BACKEND": "channels_redis.pubsub.RedisPubSubChannelLayer",
"CONFIG": {"hosts": [f'{BROKER_URL}:6379']},
},
}

Expand Down
4 changes: 2 additions & 2 deletions config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter

from src.authentication import urls as authentication_urls
from src.application import urls as application_urls
from src.user import urls as user_urls
from src.handlers import urls as handler_urls
from src.download import urls as download_urls

from src.user.views import UserViewSet
from src.download.views import RequestViewSet
Expand All @@ -33,9 +33,9 @@

urlpatterns = [
path("admin", admin.site.urls),
path("api/authentication/", include(authentication_urls)),
path("api/application/", include(application_urls)),
path("api/users/", include(user_urls)),
path("api/handlers/", include(handler_urls)),
path("api/download/", include(download_urls)),
path("api/", include(router.urls)),
]
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ daphne==4.0.0
channels==4.0.0
channels-redis==4.0.0
requests==2.28.1
python-magic==0.4.27
markdown==3.4.1
celery==5.2.7
redis==4.3.4
Expand Down
6 changes: 5 additions & 1 deletion src/application/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
This file contains the views for the custom application endpoints.
"""
from django.http import JsonResponse
from rest_framework.decorators import api_view

from .utils import get_build_info

def build_info(request):

@api_view(['GET'])
def build_info(_):
"""
Return information about the current build of the API.
"""
Expand Down
3 changes: 0 additions & 3 deletions src/auth_token/__init__.py

This file was deleted.

16 changes: 0 additions & 16 deletions src/auth_token/apps.py

This file was deleted.

3 changes: 3 additions & 0 deletions src/authentication/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Authentication package init.
"""
2 changes: 1 addition & 1 deletion src/auth_token/admin.py → src/authentication/admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Auth token admin.
Authentication admin.
This file defines the Django admin structure for the package models. For more info see
https://docs.djangoproject.com/en/3.0/ref/django-admin/
Expand Down
16 changes: 16 additions & 0 deletions src/authentication/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
Authentication app config.
This file defines the authentication app configuration. For more information see
https://docs.djangoproject.com/en/3.0/ref/applications/#configuring-applications
"""
from django.apps import AppConfig


class AuthenticationAppConfig(AppConfig):
"""
Authentication app config.
"""

name = "src.authentication"
verbose_name = "Authentication"
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,20 @@ class CookieTokenAuthentication(TokenAuthentication):
"""

def authenticate(self, request):
# Check if 'token_auth' is in the request query params.
# Give precedence to 'Authorization' header.
"""
Check if 'token_auth' is in the request query params.
Give precedence to 'Authorization' header.
"""
if (
"auth_token" in request.COOKIES
and "HTTP_AUTHORIZATION" not in request.META
):
return self.authenticate_credentials(request.COOKIES.get("auth_token"))
else:
return super(CookieTokenAuthentication, self).authenticate(request)

def get_user_by_cookie_auth_token(self, request):
"""
Retrieve the user and token based on a cookie auth token.
"""
return self.authenticate_credentials(request.COOKIES.get("auth_token"))
11 changes: 11 additions & 0 deletions src/authentication/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""
Authentication urls.
This file contains url definitions for authentication endpoints.
"""
from django.urls import path
from .views import VerifyFileAccessView

urlpatterns = [
path("verify/file", VerifyFileAccessView.as_view()),
]
51 changes: 51 additions & 0 deletions src/authentication/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""
Authentication views.
This file contains the views for the custom authentication endpoints.
"""
import json

from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from rest_framework.response import Response
from rest_framework.views import APIView

from src.authentication.authentication import CookieTokenAuthentication
from src.download.utils import list_files, prepare_path, validate_for_request, validate_for_archive, log_file_access


class VerifyFileAccessView(APIView):
"""
An API view for checking file access.
"""
authentication_classes = [CookieTokenAuthentication]

def get(self, request):
"""
Validate a nginx subrequest cookie auth token and subsequent file access for the given auth token user.
:param request: Request
:return: Response
"""
user, _ = self.get_authenticators()[0].get_user_by_cookie_auth_token(request)
path = prepare_path(request.headers.get('X-Original-Uri')[1:])

is_valid_for_request = validate_for_request(path, user)
is_valid_for_archive = validate_for_archive(path, user)

if is_valid_for_request or is_valid_for_archive:
if is_valid_for_request:
file_log = log_file_access(path)
async_to_sync(get_channel_layer().group_send)(
f"requests.group.{user.id}",
{
"type": "websocket.send",
"data": {
"type": "requests.files.retrieved",
"message": json.dumps(list_files(file_log.request.path), default=str),
},
},
)
return Response(status=200)

return Response(status=403, data="You're not authorized to access this resource or the resource doesn't exist.")
14 changes: 0 additions & 14 deletions src/download/urls.py

This file was deleted.

44 changes: 2 additions & 42 deletions src/download/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,10 @@
This file contains functions and action not fit for standard Django files.
"""
import os
import magic

from base64 import b64decode
from urllib.parse import unquote
from datetime import datetime

from wsgiref.util import FileWrapper
from django.http import FileResponse

from src.user.models import User
from .models import BaseRequest, FilesLog

Expand Down Expand Up @@ -84,10 +79,7 @@ def prepare_path(path: str) -> str:
:param path: The raw user provided path value.
:return: A decoded and normalized path.
"""
path = unquote(b64decode(path).decode('utf-8'))
path = os.path.normpath(path)

return path
return os.path.normpath(path)


def validate_user_path(path_parts: list, user: User) -> bool:
Expand Down Expand Up @@ -174,37 +166,5 @@ def log_file_access(path: str) -> FilesLog:
"""
:param path: A str containing the relative file path.
"""
path = unquote(path)
return FilesLog.objects.create(request=get_request(path), path=path)


def create_file_streaming_response(path: str) -> FileResponse:
"""
Create a streaming file response to serve the file while
reducing the memory usage in order to support large file
downloads, particularly on memory limited hardware.
The file will always be force downloaded as attachment if
the file size exceeds a given limit, else it is up to the
client to decide how to process and view the file.
:param path: A str containing the relative file path.
:return: A FileResponse containing a streaming file.
"""
filename = os.path.basename(path)
filename = filename.replace(',', '').replace(';', '-')
file_size = os.path.getsize(path)
mime = magic.Magic(mime=True)
attachment = file_size > 5000000 # 5 MB
chunk_size = 32000 # 32 KB

response = FileResponse(
FileWrapper(open(path, "rb", buffering=chunk_size), blksize=chunk_size),
as_attachment=attachment
)

response["Content-Length"] = os.path.getsize(path)
response["Content-Type"] = mime.from_file(path)
response[
"Content-Disposition"
] = f"{'attachment' if attachment else 'inline'}; filename=\"{filename}\""

return response
47 changes: 1 addition & 46 deletions src/download/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,17 @@
and use of the polymorphic serializers all registered
handler Requests are automatically handled by this viewset.
"""
import json

from django.db.models import QuerySet
from rest_framework import viewsets, mixins
from rest_framework.response import Response
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import TokenAuthentication
from rest_framework.views import APIView
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer

from .models import BaseRequest
from .serializers import PolymorphicRequestSerializer, RequestLogSerializer
from .tasks import download_request, compress_request
from .utils import list_files, prepare_path, validate_for_request, validate_for_archive, log_file_access,\
create_file_streaming_response
from src.auth_token.authentication import CookieTokenAuthentication
from .utils import list_files


class RequestViewSet(
Expand Down Expand Up @@ -137,41 +130,3 @@ def compress(self, request, pk=None) -> Response:
return Response(serializer.data)


class GetFileView(APIView):
"""
An APIView for retrieving a file.
"""

authentication_classes = [CookieTokenAuthentication]
permission_classes = [IsAuthenticated]

def get(self, request, path, *args, **kwargs):
"""
Retrieve a single file from a download request.
:param path: user provided path
:param request: Request
:param args: *
:param kwargs: *
:return: Response|FileResponse
"""
path = prepare_path(path)
is_validate_for_request = validate_for_request(path, self.request.user)
is_validate_for_archive = validate_for_archive(path, self.request.user)

if is_validate_for_request or is_validate_for_archive:
if is_validate_for_request:
file_log = log_file_access(path)
async_to_sync(get_channel_layer().group_send)(
f"requests.group.{request.user.id}",
{
"type": "websocket.send",
"data": {
"type": "requests.files.retrieved",
"message": json.dumps(list_files(file_log.request.path), default=str),
},
},
)
return create_file_streaming_response(path)

return Response(status=403, data="You're not authorized to access this resource or the resource doesn't exist.")

0 comments on commit 9520908

Please sign in to comment.