From 11f33611ae9c5d887b18b4e4cb562023473dbe6c Mon Sep 17 00:00:00 2001 From: odysseusmax Date: Sun, 13 Jun 2021 16:31:23 +0530 Subject: [PATCH] add basic auth support and few under the hood changes --- README.md | 2 +- app/config.py | 2 + app/main.py | 11 ++++- app/routes.py | 27 ++++++------ app/templates/index.html | 82 ++++++++++++++++++------------------ app/templates/js/playlist.js | 23 ++-------- app/util.py | 15 ++++--- app/views/index_view.py | 14 ++++-- app/views/info_view.py | 2 +- app/views/login_view.py | 30 ++++--------- app/views/logout_view.py | 11 +++-- app/views/middlewhere.py | 65 ++++++++++++++++------------ requirements.txt | 1 + 13 files changed, 144 insertions(+), 141 deletions(-) diff --git a/README.md b/README.md index e9b510c..d63308f 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ pip3 install -U -r requirements.txt | `PASSWORD` (optional) | Password for authentication, defaults to `''`. | `SHORT_URL_LEN` (optional) | Url length for aliases | `SESSION_COOKIE_LIFETIME` (optional) | Number of minutes, for which authenticated session is valid for, after which user has to login again. defaults to 60. -| `SECRET_KEY` (optional) | Long string for signing the session cookies, required if authentication is enabled. +| `SECRET_KEY` (optional) | 32 characters long string for signing the session cookies, required if authentication is enabled. * **Setting value for `INDEX_SETTINGS`** diff --git a/app/config.py b/app/config.py index a513e53..c74cf30 100644 --- a/app/config.py +++ b/app/config.py @@ -55,6 +55,8 @@ authenticated = username and password SESSION_COOKIE_LIFETIME = int(os.environ.get("SESSION_COOKIE_LIFETIME") or "60") try: SECRET_KEY = os.environ["SECRET_KEY"] + if len(SECRET_KEY) != 32: + raise ValueError("SECRET_KEY should be exactly 32 charaters long") except (KeyError, ValueError): if authenticated: traceback.print_exc() diff --git a/app/main.py b/app/main.py index d9dbc8b..52b51fc 100644 --- a/app/main.py +++ b/app/main.py @@ -5,6 +5,8 @@ import logging import aiohttp_jinja2 import jinja2 from aiohttp import web +from aiohttp_session import session_middleware +from aiohttp_session.cookie_storage import EncryptedCookieStorage from .telegram import Client from .routes import setup_routes @@ -33,6 +35,13 @@ class Indexer: def __init__(self): self.server = web.Application( middlewares=[ + session_middleware( + EncryptedCookieStorage( + secret_key=SECRET_KEY.encode(), + max_age=60 * SESSION_COOKIE_LIFETIME, + cookie_name="TG_INDEX_SESSION" + ) + ), middleware_factory(), ] ) @@ -42,8 +51,6 @@ class Indexer: self.server["is_authenticated"] = authenticated self.server["username"] = username self.server["password"] = password - self.server["SESSION_COOKIE_LIFETIME"] = SESSION_COOKIE_LIFETIME - self.server["SECRET_KEY"] = SECRET_KEY async def startup(self): await self.tg_client.start() diff --git a/app/routes.py b/app/routes.py index 487fd88..758bce0 100644 --- a/app/routes.py +++ b/app/routes.py @@ -7,6 +7,7 @@ from .config import index_settings log = logging.getLogger(__name__) + async def setup_routes(app, handler): h = handler client = h.client @@ -22,17 +23,17 @@ async def setup_routes(app, handler): web.post("/login", h.login_post, name="login_handle"), web.get("/logout", h.logout_get, name="logout"), ] + def get_common_routes(p): return [ - web.get(p, h.index), - web.get(p + r"/logo", h.logo), - web.get(p + r"/{id:\d+}/view", h.info), - web.get(p + r"/{id:\d+}/download", h.download_get), - web.head(p + r"/{id:\d+}/download", h.download_head), - web.get(p + r"/{id:\d+}/thumbnail", h.thumbnail_get), - web.get(p + r"/{id:\d+}/v.mp4", h.download_get), - web.head(p + r"/{id:\d+}/v.mp4", h.download_head), - ] + web.get(p, h.index), + web.get(p + r"/logo", h.logo), + web.get(p + r"/{id:\d+}/view", h.info), + web.get(p + r"/{id:\d+}/thumbnail", h.thumbnail_get), + web.get(p + r"/{id:\d+}/{filename}", h.download_get), + web.head(p + r"/{id:\d+}/{filename}", h.download_head), + ] + if index_all: # print(await client.get_dialogs()) # dialogs = await client.get_dialogs() @@ -56,9 +57,7 @@ async def setup_routes(app, handler): alias_id = h.generate_alias_id(chat) p = "/{chat:" + alias_id + "}" - routes.extend( - get_common_routes(p) - ) + routes.extend(get_common_routes(p)) log.debug(f"Index added for {chat.id} at /{alias_id}") else: @@ -66,9 +65,7 @@ async def setup_routes(app, handler): chat = await client.get_entity(chat_id) alias_id = h.generate_alias_id(chat) p = "/{chat:" + alias_id + "}" - routes.extend( - get_common_routes(p) # returns list() of common routes - ) + routes.extend(get_common_routes(p)) # returns list() of common routes log.debug(f"Index added for {chat.id} at /{alias_id}") routes.append(web.view(r"/{wildcard:.*}", h.wildcard)) app.add_routes(routes) diff --git a/app/templates/index.html b/app/templates/index.html index 56a221e..649c241 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -12,7 +12,7 @@
-

{{name}}

+ {{name}}
@@ -52,57 +52,59 @@ href="{{item.url}}" class="text-sm flex flex-col items-center justify-center w-full min-h-full md:w-1/5 lg:w-1/6 rounded my-4 md:mx-1 shadow hover:shadow-md border-solid "> -
{{item.file_id}}
- {% if item.media %} - + + + + +
{{item.insight}}
+ + {% if not block_downloads %} + + + + + + - - + {% if 'video' in item.mime_type %} + + + + + + + -
{{item.insight}}
+ + + {% endif %} + {% endif %} - - - - - - - - - - - - - - - - - - -
+
{% else %}
{{item.insight}}
{% endif %} +
{{item.file_id}}
diff --git a/app/templates/js/playlist.js b/app/templates/js/playlist.js index ff73986..0fe3cec 100644 --- a/app/templates/js/playlist.js +++ b/app/templates/js/playlist.js @@ -1,26 +1,9 @@ -function singleItemPlaylist(file,name){ - let hostUrl = 'http://' + window.location.host +function singleItemPlaylist(file,name,basicAuth){ + let hostUrl = `http://${basicAuth}${window.location.host}` let pd = "" pd += '#EXTM3U\n' pd += `#EXTINF: ${name}\n` pd += `${hostUrl}/${file}\n` let blob = new Blob([pd], { endings: "native" }); saveAs(blob, `${name}.m3u`); -} - -// function createPlaylist(indexSite, id, playlistName = "Playlist", duration = 60) { -// let pd = "" -// name = playlistName -// pd += '#EXTM3U\n' -// pd += `#EXTINF: ${duration * 60} | ${name}\n` -// pd += `${indexSite}/${id}/v.mp4\n` -// return pd -// } - -// function playlist(id, name) { -// hostUrl = 'https://' + window.location.host -// playlistData = createPlaylist(hostUrl, id, name); -// let blob = new Blob([playlistData], { endings: "native" }); -// saveAs(blob, `${name}.m3u`); -// } - +} diff --git a/app/util.py b/app/util.py index 7be54e4..91a2cbb 100644 --- a/app/util.py +++ b/app/util.py @@ -1,14 +1,19 @@ +from urllib.parse import quote + + def get_file_name(message): if message.file.name: - return message.file.name.replace('\n', ' ') - ext = message.file.ext or "" - return f"{message.date.strftime('%Y-%m-%d_%H:%M:%S')}{ext}" + name = message.file.name + else: + ext = message.file.ext or "" + name = f"{message.date.strftime('%Y-%m-%d_%H:%M:%S')}{ext}" + return quote(name) def get_human_size(num): base = 1024.0 - sufix_list = ['B','KiB','MiB','GiB','TiB','PiB','EiB','ZiB', 'YiB'] + sufix_list = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"] for unit in sufix_list: if abs(num) < base: return f"{round(num, 2)} {unit}" - num /= base \ No newline at end of file + num /= base diff --git a/app/views/index_view.py b/app/views/index_view.py index faa7fbb..9e1a048 100644 --- a/app/views/index_view.py +++ b/app/views/index_view.py @@ -4,7 +4,7 @@ import aiohttp_jinja2 from telethon.tl import types -from app.config import results_per_page +from app.config import results_per_page, block_downloads from app.util import get_file_name, get_human_size @@ -51,16 +51,18 @@ class IndexView: for m in messages: entry = None if m.file and not isinstance(m.media, types.MessageMediaWebPage): + filename = get_file_name(m) + insight = m.text[:60] if m.text else filename entry = dict( file_id=m.id, media=True, thumbnail=f"/{alias_id}/{m.id}/thumbnail", mime_type=m.file.mime_type, - insight=get_file_name(m), + filename=filename, + insight=insight, human_size=get_human_size(m.file.size), url=f"/{alias_id}/{m.id}/view", - download=f"{alias_id}/{m.id}/download", - vlc = f"{alias_id}/{m.id}/v.mp4", + download=f"{alias_id}/{m.id}/{filename}", ) elif m.message: entry = dict( @@ -100,4 +102,8 @@ class IndexView: "logo": f"/{alias_id}/logo", "title": "Index of " + chat["title"], "authenticated": req.app["is_authenticated"], + "block_downloads": block_downloads, + "m3u_option": "" + if not req.app["is_authenticated"] + else f"{req.app['is_authenticated']}:{req.app['is_authenticated']}@", } diff --git a/app/views/info_view.py b/app/views/info_view.py index 31091ee..e275f8b 100644 --- a/app/views/info_view.py +++ b/app/views/info_view.py @@ -78,7 +78,7 @@ class InfoView: "thumbnail": f"/{alias_id}/{file_id}/thumbnail", "download_url": "#" if block_downloads - else f"/{alias_id}/{file_id}/download", + else f"/{alias_id}/{file_id}/{file_name}", "page_id": alias_id, "block_downloads": block_downloads, } diff --git a/app/views/login_view.py b/app/views/login_view.py index 1229259..2288e33 100644 --- a/app/views/login_view.py +++ b/app/views/login_view.py @@ -1,9 +1,8 @@ import time -import hmac -import hashlib from aiohttp import web import aiohttp_jinja2 +from aiohttp_session import new_session class LoginView: @@ -20,32 +19,21 @@ class LoginView: if "username" not in post_data: loc = location.update_query({"error": "Username missing"}) - raise web.HTTPFound(location=loc) + return web.HTTPFound(location=loc) if "password" not in post_data: loc = location.update_query({"error": "Password missing"}) - raise web.HTTPFound(location=loc) + return web.HTTPFound(location=loc) authenticated = (post_data["username"] == req.app["username"]) and ( post_data["password"] == req.app["password"] ) if not authenticated: loc = location.update_query({"error": "Wrong Username or Passowrd"}) - raise web.HTTPFound(location=loc) + return web.HTTPFound(location=loc) - resp = web.Response(status=302, headers={"Location": redirect_to}) - now = time.time() - resp.set_cookie( - name="_tgindex_session", - value=str(now), - max_age=60 * req.app["SESSION_COOKIE_LIFETIME"], - ) - digest = hmac.new( - req.app["SECRET_KEY"].encode(), str(now).encode(), hashlib.sha256 - ).hexdigest() - resp.set_cookie( - name="_tgindex_secret", - value=digest, - max_age=60 * req.app["SESSION_COOKIE_LIFETIME"], - ) - return resp + session = await new_session(req) + print(session) + session["logged_in"] = True + session["logged_in_at"] = time.time() + return web.HTTPFound(location=redirect_to) diff --git a/app/views/logout_view.py b/app/views/logout_view.py index 6dbe71a..db851c9 100644 --- a/app/views/logout_view.py +++ b/app/views/logout_view.py @@ -1,11 +1,10 @@ +from aiohttp_session import get_session from aiohttp import web class LogoutView: async def logout_get(self, req): - resp = web.Response( - status=302, headers={"Location": str(req.app.router["home"].url_for())} - ) - resp.del_cookie(name="_tgindex_session") - resp.del_cookie(name="_tgindex_secret") - return resp + session = await get_session(req) + session["logged_in"] = False + + return web.HTTPFound(req.app.router["home"].url_for()) diff --git a/app/views/middlewhere.py b/app/views/middlewhere.py index 57a4dc0..4e5232d 100644 --- a/app/views/middlewhere.py +++ b/app/views/middlewhere.py @@ -1,14 +1,42 @@ import time -import hmac -import hashlib import logging from aiohttp.web import middleware, HTTPFound +from aiohttp import BasicAuth, hdrs +from aiohttp_session import get_session log = logging.getLogger(__name__) +def _do_basic_auth_check(request): + auth_header = request.headers.get(hdrs.AUTHORIZATION) + if not auth_header: + return + + try: + auth = BasicAuth.decode(auth_header=auth_header) + except ValueError: + auth = None + + if not auth: + return + + if auth.login is None or auth.password is None: + return + + return True + + +async def _do_cookies_auth_check(request): + session = await get_session(request) + if not session.get("logged_in", False): + return + + session["last_at"] = time.time() + return True + + def middleware_factory(): @middleware async def factory(request, handler): @@ -16,36 +44,21 @@ def middleware_factory(): "/login", "/logout", ]: - cookies = request.cookies url = request.app.router["login_page"].url_for() if str(request.rel_url) != "/": url = url.with_query(redirect_to=str(request.rel_url)) - if any(x not in cookies for x in ("_tgindex_session", "_tgindex_secret")): - raise HTTPFound(url) + basic_auth_check_resp = _do_basic_auth_check(request) - tgindex_session = cookies["_tgindex_session"] - tgindex_secret = cookies["_tgindex_secret"] - calculated_digest = hmac.new( - request.app["SECRET_KEY"].encode(), - str(tgindex_session).encode(), - hashlib.sha256, - ).hexdigest() - if tgindex_secret != calculated_digest: - raise HTTPFound(url) + if basic_auth_check_resp is not None: + return await handler(request) - try: - created_at = ( - float(tgindex_session) + request.app["SESSION_COOKIE_LIFETIME"] - ) - if ( - time.time() - > created_at + 60 * request.app["SESSION_COOKIE_LIFETIME"] - ): - raise HTTPFound(url) - except Exception as e: - log.error(e, exc_info=True) - raise HTTPFound(url) + cookies_auth_check_resp = await _do_cookies_auth_check(request) + + if cookies_auth_check_resp is not None: + return await handler(request) + + return HTTPFound(url) return await handler(request) diff --git a/requirements.txt b/requirements.txt index 42ed3cf..81a07c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ aiohttp-jinja2 telethon>=1.16.4 cryptg pillow +aiohttp_session[secure]