add basic auth support and few under the hood changes
This commit is contained in:
parent
9eabc78872
commit
11f33611ae
|
@ -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`**
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
11
app/main.py
11
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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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">
|
||||
|
||||
<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>
|
||||
|
||||
<!-- Buttons container -->
|
||||
<span class="item-buttons my-1 rounded shadow-inner">
|
||||
{% 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>
|
||||
|
||||
<div class="p-4 text-dark py-0 px-2 my-1 ">{{item.insight}}</div>
|
||||
<!-- 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 %}
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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.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>
|
||||
|
||||
|
|
|
@ -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`);
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
15
app/util.py
15
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
|
||||
num /= base
|
||||
|
|
|
@ -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']}@",
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -3,3 +3,4 @@ aiohttp-jinja2
|
|||
telethon>=1.16.4
|
||||
cryptg
|
||||
pillow
|
||||
aiohttp_session[secure]
|
||||
|
|
Loading…
Reference in New Issue