add basic auth support and few under the hood changes

This commit is contained in:
odysseusmax 2021-06-13 16:31:23 +05:30
parent 9eabc78872
commit 11f33611ae
13 changed files with 144 additions and 141 deletions

View File

@ -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`**

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -12,7 +12,7 @@
<div
class="m-2 block md:flex items-center justify-center md:justify-start text-2xl md:text-right font-bold text-blue-500">
<img class="mx-auto md:ml-0 md:mr-1 my-2 w-16 h-16 rounded-full bg-black outline-none" src="{{logo}}">
<h1> {{name}} </h1>
<a href="javascript:window.location.href=window.location.href"> {{name}} </a>
</div>
<div class="m-2">
@ -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 ">
<div class="bg-blue-500 rounded text-white my-1 py-0 px-1">{{item.file_id}}</div>
{% if item.media %}
<a href="{{item.url}}"><img src="{{item.thumbnail}}" class="w-full rounded shadow-inner"></a>
<a href="{{item.url}}"><img src="{{item.thumbnail}}" class="w-full rounded shadow-inner"></a>
<!-- Buttons container -->
<span class="item-buttons my-1 rounded shadow-inner">
<!-- Buttons container -->
<span class="item-buttons my-1 rounded shadow-inner">
<div class="p-4 text-dark py-0 px-2 my-1 ">{{item.insight}}</div>
<div class="p-4 text-dark py-0 px-2 my-1 ">{{item.insight}}</div>
{% if not block_downloads %}
<!-- Direct file download button -->
<a href="{{item.download}}"
class=" hover:bg-blue-300 text-gray-900 font-semibold py-1 px-2 rounded inline-flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 13l-3 3m0 0l-3-3m3 3V8m0 13a9 9 0 110-18 9 9 0 010 18z" />
</svg>
</a>
<!-- Direct file download button -->
<a href="{{item.download}}"
class=" hover:bg-blue-300 text-gray-900 font-semibold py-1 px-2 rounded inline-flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 13l-3 3m0 0l-3-3m3 3V8m0 13a9 9 0 110-18 9 9 0 010 18z" />
</svg>
</a>
{% if 'video' in item.mime_type %}
<!-- Kodi/media player supported url ending with v.mp4 -->
<a title="v.mp4" href="{{item.download}}"
class=" hover:bg-blue-300 text-gray-900 font-semibold py-1 px-2 rounded inline-flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</a>
<!-- Kodi/media player supported url ending with v.mp4 -->
<a title="v.mp4" href="{{item.vlc}}"
class=" hover:bg-blue-300 text-gray-900 font-semibold py-1 px-2 rounded inline-flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</a>
<!-- One click single item playlist Download -->
<button title="{{item.filename}}.m3u" onclick="singleItemPlaylist('{{item.download}}','{{item.filename}}', '{{m3u_option}}');"
class=" hover:bg-blue-300 text-gray-900 font-semibold py-1 px-2 rounded inline-flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 4h13M3 8h9m-9 4h9m5-4v12m0 0l-4-4m4 4l4-4" />
</svg>
</button>
{% endif %}
{% endif %}
<!-- One click single item playlist Download -->
<button title="{{item.insight}}.m3u" onclick='singleItemPlaylist("{{item.vlc}}","{{item.insight}}")'
class=" hover:bg-blue-300 text-gray-900 font-semibold py-1 px-2 rounded inline-flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 4h13M3 8h9m-9 4h9m5-4v12m0 0l-4-4m4 4l4-4" />
</svg>
</button>
</span>
</span>
{% else %}
<a href={{item.url}}>
<div class="p-4 rounded shadow-inner rounded text-dark py-0 px-2">{{item.insight}}</div>
</a>
{% endif %}
<div class="bg-blue-500 rounded text-white my-1 py-0 px-1">{{item.file_id}}</div>
</div>

View File

@ -1,5 +1,5 @@
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`
@ -7,20 +7,3 @@ function singleItemPlaylist(file,name){
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`);
// }

View File

@ -1,13 +1,18 @@
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}"

View File

@ -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']}@",
}

View File

@ -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,
}

View File

@ -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)

View File

@ -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())

View File

@ -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)

View File

@ -3,3 +3,4 @@ aiohttp-jinja2
telethon>=1.16.4
cryptg
pillow
aiohttp_session[secure]