diff --git a/Pipfile.lock b/Pipfile.lock index 136d57fe..3432b88e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -366,11 +366,11 @@ }, "croniter": { "hashes": [ - "sha256:f1f8ca0af64212fbe99b1bee125ee5a1b53a9c1b433968d8bca8817b79d237f3", - "sha256:fdbb44920944045cc323db54599b321325141d82d14fa7453bc0699826bbe9ed" + "sha256:1041b912b4b1e03751a0993531becf77851ae6e8b334c9c76ffeffb8f055f53f", + "sha256:f15e80828d23920c4bb7f4d9340b932c9dcabecafc7775703c8b36d1253ed526" ], "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.0.5" + "version": "==2.0.7" }, "debugpy": { "hashes": [ @@ -403,11 +403,11 @@ }, "exceptiongroup": { "hashes": [ - "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad", - "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16" + "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", + "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" ], "markers": "python_version < '3.11'", - "version": "==1.2.1" + "version": "==1.2.2" }, "frozenlist": { "hashes": [ diff --git a/README.md b/README.md index 4cdc7182..7126c636 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ YTPTube started as a fork of [meTube](https://github.com/alexta69/metube), Since * Tasks Runner. It allow you to queue channels for downloading using simple `json` file. * Webhook sender. It allow you to add webhook endpoints that receive events related to downloads using simple `json` file. * Multi-downloads support. - +* Basic Authentication support. + ### Tips Your `yt-dlp` config should include the following options for optimal working conditions. @@ -78,6 +79,8 @@ Certain values can be set via environment variables, using the `-e` parameter on * __YTP_MAX_WORKERS__: How many works to use for downloads. Defaults to `1`. * __YTP_STREAMER_VCODEC__: The video codec to use for in-browser streaming. Defaults to `libx264`. * __YTP_STREAMER_ACODEC__: The audio codec to use for in-browser streaming. Defaults to `aac`. +* __YTP_AUTH_USERNAME__: Username for basic authentication. Defaults open for all +* __YTP_AUTH_PASSWORD__: Password for basic authentication. Defaults open for all. ## Running behind a reverse proxy @@ -285,6 +288,14 @@ The `config/webhooks.json`, is a json file, which can be used to add webhook end ] ``` +## Authentication + +To enable basic authentication, set the `YTP_AUTH_USERNAME` and `YTP_AUTH_PASSWORD` environment variables. And restart the container. +This will prompt the user to enter the username and password before accessing the web interface/API. +As this is a simple basic authentication, if your browser doesn't show the prompt, you can use the following URL + +`http://username:password@your_ytptube_url:port` + # Social contact If you have short or quick questions, you are free to join my [discord server](https://discord.gg/G3GpVR8xpb) and ask diff --git a/app/Config.py b/app/Config.py index 95653bda..979abb69 100644 --- a/app/Config.py +++ b/app/Config.py @@ -57,6 +57,9 @@ class Config: streamer_vcodec = 'libx264' streamer_acodec = 'aac' + auth_username: str = None + auth_password: str = None + ytdlp_version: str = YTDLP_VERSION _int_vars: tuple = ('port', 'max_workers', 'socket_timeout', 'extract_info_timeout',) @@ -170,6 +173,9 @@ class Config: LOG.info(f'Keep temp: {self.temp_keep}') + if self.auth_password and self.auth_username: + LOG.warn(f"Basic Auth enabled with username: '{self.auth_username}'.") + self.started = time.time() def _getAttributes(self) -> dict: diff --git a/app/main.py b/app/main.py index 76264b2e..5b5fe9b6 100644 --- a/app/main.py +++ b/app/main.py @@ -1,17 +1,17 @@ #!/usr/bin/env python3 import asyncio +import base64 from datetime import datetime import json import os import random -import selectors import time from Config import Config from DownloadQueue import DownloadQueue from Utils import ObjectSerializer, Notifier, isDownloaded, load_file from aiohttp import web, client -from aiohttp.web import Request, Response +from aiohttp.web import Request, Response, RequestHandler from Webhooks import Webhooks from player.M3u8 import M3u8 from player.Segments import Segments @@ -20,7 +20,6 @@ import logging import caribou import sqlite3 from aiocron import crontab -from aiohttp.helpers import parse_http_date import magic LOG = logging.getLogger('app') @@ -91,6 +90,12 @@ class Main: self.serializer = ObjectSerializer() self.app = web.Application() + + if self.config.auth_username and self.config.auth_password: + self.app.middlewares.append( + Main.basic_auth(self.config.auth_username, self.config.auth_password) + ) + self.sio = socketio.AsyncServer(cors_allowed_origins='*') self.sio.attach(self.app, socketio_path=self.config.url_prefix + 'socket.io') @@ -120,6 +125,31 @@ class Main: print=lambda _: LOG.info(start) ) + def basic_auth(username: str, password: str): + @web.middleware + async def middleware_handler(request: Request, handler: RequestHandler) -> Response: + auth_header = request.headers.get('Authorization') + + if auth_header is None: + return web.Response(status=401, headers={ + 'WWW-Authenticate': 'Basic realm="Authorization Required."', + }, text='Unauthorized.') + + auth_type, encoded_credentials = auth_header.split(' ', 1) + + if 'basic' != auth_type.lower(): + return web.Response(status=401, text="Unsupported authentication method.") + + decoded_credentials = base64.b64decode(encoded_credentials).decode('utf-8') + user_name, _, user_password = decoded_credentials.partition(':') + + if user_name != username or user_password != password: + return web.Response(status=401, text='Unauthorized (Invalid credentials).') + + return await handler(request) + + return middleware_handler + async def connect(self, sid, _): data: dict = { **self.queue.get(),