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 = "*"
yt-dlp = "*"
anyio = "*"
pycryptodome = "*"
[dev-packages]

53
Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "17668280c8e863033313b83f64240f6837e733957fbc2613767d297fee6c6fc2"
"sha256": "9fd5fe76e070040f587d879d9190d2bda03ef0a45a8ac970f408cb57bd578349"
},
"pipfile-spec": 6,
"requires": {
@ -27,11 +27,11 @@
},
"aiohappyeyeballs": {
"hashes": [
"sha256:19728772cb12263077982d2f55453babd8bec6a052a926cd5c0c42796da8bf62",
"sha256:6cac4f5dd6e34a9644e69cf9021ef679e4394f54e58a183056d12009e42ea9e3"
"sha256:0850b580748c7071db98bffff6d4c94028d0d3035acc20fd721a0ce7e8cac35d",
"sha256:18fde6204a76deeabc97c48bdd01d5801cfda5d6b9c8bbeb1aaaee9d648ca191"
],
"markers": "python_version >= '3.9'",
"version": "==2.4.8"
"version": "==2.5.0"
},
"aiohttp": {
"hashes": [
@ -842,6 +842,45 @@
"markers": "python_version >= '3.8'",
"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": {
"hashes": [
"sha256:00361a0aaabc3162992087d83bc067517c95c35810b631d9a40b80732003066e",
@ -1159,11 +1198,11 @@
},
"tzlocal": {
"hashes": [
"sha256:2fafbfc07e9d8b49ade18f898d6bcd37ae88ce3ad6486842a2e4f03af68323d2",
"sha256:3814135a1bb29763c6e4f08fd6e41dbb435c7a60bfbb03270211bcc537187d8c"
"sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd",
"sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d"
],
"markers": "python_version >= '3.9'",
"version": "==5.3"
"version": "==5.3.1"
},
"wsproto": {
"hashes": [

View file

@ -9,7 +9,7 @@ import random
import time
import uuid
from collections.abc import Awaitable
from datetime import UTC, datetime
from datetime import UTC, datetime, timedelta
from pathlib import Path
from urllib.parse import quote
@ -38,6 +38,8 @@ from .Utils import (
IGNORED_KEYS,
StreamingError,
arg_converter,
decrypt_data,
encrypt_data,
get_file,
get_mime_type,
get_sidecar_subtitles,
@ -281,6 +283,18 @@ class HttpAPI(Common):
if auth_header is None and request.query.get("apikey") is not None:
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:
return web.json_response(
status=web.HTTPUnauthorized.status_code,
@ -309,7 +323,26 @@ class HttpAPI(Common):
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

View file

@ -1,3 +1,4 @@
import base64
import copy
import glob
import ipaddress
@ -13,6 +14,7 @@ from functools import lru_cache
from typing import Any
import yt_dlp
from Crypto.Cipher import AES
from yt_dlp.networking.impersonate import ImpersonateTarget
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 (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 multiprocessing
import os
@ -141,6 +140,9 @@ class Config:
sentry_dsn: str | None = None
"The Sentry DSN to use for error reporting."
secret_key: str
"The secret key to use for the application."
_manual_vars: tuple = (
"temp_path",
"config_path",
@ -347,6 +349,18 @@ class Config:
handler.setFormatter(formatter)
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()
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": "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' }
},

View file

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