for basic auth rely on cookies and not just the Authorization header

This commit is contained in:
ArabCoders 2025-03-07 01:01:05 +03:00
parent 0b04f098de
commit 6e52b3c356
7 changed files with 142 additions and 16 deletions

View file

@ -23,6 +23,7 @@ brotli = "*"
brotlicffi = "*" brotlicffi = "*"
yt-dlp = "*" yt-dlp = "*"
anyio = "*" anyio = "*"
pycryptodome = "*"
[dev-packages] [dev-packages]

53
Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "17668280c8e863033313b83f64240f6837e733957fbc2613767d297fee6c6fc2" "sha256": "9fd5fe76e070040f587d879d9190d2bda03ef0a45a8ac970f408cb57bd578349"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -27,11 +27,11 @@
}, },
"aiohappyeyeballs": { "aiohappyeyeballs": {
"hashes": [ "hashes": [
"sha256:19728772cb12263077982d2f55453babd8bec6a052a926cd5c0c42796da8bf62", "sha256:0850b580748c7071db98bffff6d4c94028d0d3035acc20fd721a0ce7e8cac35d",
"sha256:6cac4f5dd6e34a9644e69cf9021ef679e4394f54e58a183056d12009e42ea9e3" "sha256:18fde6204a76deeabc97c48bdd01d5801cfda5d6b9c8bbeb1aaaee9d648ca191"
], ],
"markers": "python_version >= '3.9'", "markers": "python_version >= '3.9'",
"version": "==2.4.8" "version": "==2.5.0"
}, },
"aiohttp": { "aiohttp": {
"hashes": [ "hashes": [
@ -842,6 +842,45 @@
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==2.22" "version": "==2.22"
}, },
"pycryptodome": {
"hashes": [
"sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8",
"sha256:0fa0a05a6a697ccbf2a12cec3d6d2650b50881899b845fac6e87416f8cb7e87d",
"sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0",
"sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93",
"sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4",
"sha256:26412b21df30b2861424a6c6d5b1d8ca8107612a4cfa4d0183e71c5d200fb34a",
"sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764",
"sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca",
"sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e",
"sha256:3ba4cc304eac4d4d458f508d4955a88ba25026890e8abff9b60404f76a62c55e",
"sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd",
"sha256:590ef0898a4b0a15485b05210b4a1c9de8806d3ad3d47f74ab1dc07c67a6827f",
"sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6",
"sha256:6cce52e196a5f1d6797ff7946cdff2038d3b5f0aba4a43cb6bf46b575fd1b5bb",
"sha256:7cb087b8612c8a1a14cf37dd754685be9a8d9869bed2ffaaceb04850a8aeef7e",
"sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1",
"sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6",
"sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a",
"sha256:8acd7d34af70ee63f9a849f957558e49a98f8f1634f86a59d2be62bb8e93f71c",
"sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2",
"sha256:a1752eca64c60852f38bb29e2c86fca30d7672c024128ef5d70cc15868fa10f4",
"sha256:a3804675283f4764a02db05f5191eb8fec2bb6ca34d466167fc78a5f05bbe6b3",
"sha256:a4e74c522d630766b03a836c15bff77cb657c5fdf098abf8b1ada2aebc7d0819",
"sha256:a915597ffccabe902e7090e199a7bf7a381c5506a747d5e9d27ba55197a2c568",
"sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53",
"sha256:cc2269ab4bce40b027b49663d61d816903a4bd90ad88cb99ed561aadb3888dd3",
"sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8",
"sha256:dad9bf36eda068e89059d1f07408e397856be9511d7113ea4b586642a429a4fd",
"sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b",
"sha256:f35e442630bc4bc2e1878482d6f59ea22e280d7121d7adeaedba58c23ab6386b",
"sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297",
"sha256:ff99f952db3db2fbe98a0b355175f93ec334ba3d01bbde25ad3a5a33abc02b58"
],
"index": "pypi",
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==3.21.0"
},
"pyjson5": { "pyjson5": {
"hashes": [ "hashes": [
"sha256:00361a0aaabc3162992087d83bc067517c95c35810b631d9a40b80732003066e", "sha256:00361a0aaabc3162992087d83bc067517c95c35810b631d9a40b80732003066e",
@ -1159,11 +1198,11 @@
}, },
"tzlocal": { "tzlocal": {
"hashes": [ "hashes": [
"sha256:2fafbfc07e9d8b49ade18f898d6bcd37ae88ce3ad6486842a2e4f03af68323d2", "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd",
"sha256:3814135a1bb29763c6e4f08fd6e41dbb435c7a60bfbb03270211bcc537187d8c" "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d"
], ],
"markers": "python_version >= '3.9'", "markers": "python_version >= '3.9'",
"version": "==5.3" "version": "==5.3.1"
}, },
"wsproto": { "wsproto": {
"hashes": [ "hashes": [

View file

@ -9,7 +9,7 @@ import random
import time import time
import uuid import uuid
from collections.abc import Awaitable from collections.abc import Awaitable
from datetime import UTC, datetime from datetime import UTC, datetime, timedelta
from pathlib import Path from pathlib import Path
from urllib.parse import quote from urllib.parse import quote
@ -38,6 +38,8 @@ from .Utils import (
IGNORED_KEYS, IGNORED_KEYS,
StreamingError, StreamingError,
arg_converter, arg_converter,
decrypt_data,
encrypt_data,
get_file, get_file,
get_mime_type, get_mime_type,
get_sidecar_subtitles, get_sidecar_subtitles,
@ -281,6 +283,18 @@ class HttpAPI(Common):
if auth_header is None and request.query.get("apikey") is not None: if auth_header is None and request.query.get("apikey") is not None:
auth_header = f"Basic {request.query.get('apikey')}" auth_header = f"Basic {request.query.get('apikey')}"
if auth_header is None:
auth_cookie = request.cookies.get("auth")
if auth_cookie is not None:
try:
data = decrypt_data(auth_cookie, key=Config.get_instance().secret_key)
if data is not None:
LOG.info(f"Decoded cookie data '{data}'.")
data = base64.b64encode(data.encode()).decode()
auth_header = f"Basic {data}"
except Exception as e:
LOG.exception(e)
if auth_header is None: if auth_header is None:
return web.json_response( return web.json_response(
status=web.HTTPUnauthorized.status_code, status=web.HTTPUnauthorized.status_code,
@ -309,7 +323,26 @@ class HttpAPI(Common):
data={"error": "Unauthorized (Invalid credentials)."}, status=web.HTTPUnauthorized.status_code data={"error": "Unauthorized (Invalid credentials)."}, status=web.HTTPUnauthorized.status_code
) )
return await handler(request) response = await handler(request)
if request.path != "/":
return response
try:
response.set_cookie(
"auth",
encrypt_data(
f"{user_name}:{user_password}",
key=Config.get_instance().secret_key,
),
max_age=60 * 60 * 24 * 7,
expires=datetime.now(UTC) + timedelta(days=7),
httponly=True,
samesite="Strict",
)
except Exception as e:
LOG.exception(e)
return response
return middleware_handler return middleware_handler

View file

@ -1,3 +1,4 @@
import base64
import copy import copy
import glob import glob
import ipaddress import ipaddress
@ -13,6 +14,7 @@ from functools import lru_cache
from typing import Any from typing import Any
import yt_dlp import yt_dlp
from Crypto.Cipher import AES
from yt_dlp.networking.impersonate import ImpersonateTarget from yt_dlp.networking.impersonate import ImpersonateTarget
from .LogWrapper import LogWrapper from .LogWrapper import LogWrapper
@ -615,3 +617,44 @@ def get_file(download_path: str, file: str | pathlib.Path) -> tuple[pathlib.Path
return (realFile, 404) return (realFile, 404)
return (pathlib.Path(possibleFile), 302) return (pathlib.Path(possibleFile), 302)
def encrypt_data(data: str, key: bytes) -> str:
"""
Encrypts data using AES-GCM
Args:
data (str): The data to encrypt
key (bytes): The encryption key
Returns:
str: The encrypted data as a base64 encoded string
"""
iv = os.urandom(12) # AES-GCM requires a 12-byte IV
cipher = AES.new(key, AES.MODE_GCM, nonce=iv)
ciphertext, tag = cipher.encrypt_and_digest(data.encode())
return base64.urlsafe_b64encode(iv + ciphertext + tag).decode()
def decrypt_data(data: str, key: bytes) -> str:
"""
Decrypts AES-GCM encrypted data
Args:
data (str): The encrypted data as a base64 encoded string
key (bytes): The encryption key
Returns:
str: The decrypted data
"""
try:
data = base64.urlsafe_b64decode(data)
iv, ciphertext, tag = data[:12], data[12:-16], data[-16:]
cipher = AES.new(key, AES.MODE_GCM, nonce=iv)
plaintext = cipher.decrypt_and_verify(ciphertext, tag)
return plaintext.decode()
except Exception:
return None

View file

@ -1,4 +1,3 @@
import json
import logging import logging
import multiprocessing import multiprocessing
import os import os
@ -141,6 +140,9 @@ class Config:
sentry_dsn: str | None = None sentry_dsn: str | None = None
"The Sentry DSN to use for error reporting." "The Sentry DSN to use for error reporting."
secret_key: str
"The secret key to use for the application."
_manual_vars: tuple = ( _manual_vars: tuple = (
"temp_path", "temp_path",
"config_path", "config_path",
@ -347,6 +349,18 @@ class Config:
handler.setFormatter(formatter) handler.setFormatter(formatter)
logging.getLogger().addHandler(handler) logging.getLogger().addHandler(handler)
key_file: str = os.path.join(self.config_path, "secret.key")
# save key as bytes.
if os.path.exists(key_file) and os.path.getsize(key_file) > 5:
with open(key_file,"rb") as f:
self.secret_key = f.read().strip()
else:
self.secret_key = os.urandom(32)
with open(key_file, "wb") as f:
f.write(self.secret_key)
self.started = time.time() self.started = time.time()
def _get_attributes(self) -> dict: def _get_attributes(self) -> dict:

View file

@ -47,7 +47,7 @@ export default defineNuxtConfig({
{ "name": "viewport", "content": "width=device-width, initial-scale=1.0, maximum-scale=1.0" }, { "name": "viewport", "content": "width=device-width, initial-scale=1.0, maximum-scale=1.0" },
{ "name": "theme-color", "content": "#000000" }, { "name": "theme-color", "content": "#000000" },
], ],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }] link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico?v=100' }]
}, },
pageTransition: { name: 'page', mode: 'out-in' } pageTransition: { name: 'page', mode: 'out-in' }
}, },

View file

@ -235,14 +235,10 @@ const encodePath = item => {
*/ */
const request = (url, options = {}) => { const request = (url, options = {}) => {
const runtimeConfig = useRuntimeConfig() const runtimeConfig = useRuntimeConfig()
const token = useStorage('token', null)
options = options || {} options = options || {}
options.method = options.method || 'GET' options.method = options.method || 'GET'
options.headers = options.headers || {} options.headers = options.headers || {}
options.withCredentials = true
if (token && undefined === options.headers['Authorization']) {
options.headers['Authorization'] = 'Token ' + token.value
}
if (undefined === options.headers['Content-Type']) { if (undefined === options.headers['Content-Type']) {
if (!(options?.body instanceof FormData)) { if (!(options?.body instanceof FormData)) {