Compare commits

...

3 Commits

Author SHA1 Message Date
bd3d7509b2 Merge remote-tracking branch 'origin/master' into development 2025-06-03 20:58:58 +00:00
cb3d8efd72 final fix for telegram 2025-06-03 22:54:17 +02:00
f96f4a40a5 attempt dl_token 2025-06-03 19:57:24 +00:00
4 changed files with 83 additions and 31 deletions

60
app.py
View File

@ -348,13 +348,20 @@ def api_browse(subpath):
def serve_file(subpath): def serve_file(subpath):
# 1) Locate the real file on disk # 1) Locate the real file on disk
root, *relative_parts = subpath.split('/') root, *relative_parts = subpath.split('/')
base_path = session['folders'].get(root)
full_path = os.path.join(base_path or '', *relative_parts) dltoken = request.args.get('dltoken')
if dltoken:
as_attachment = True
full_path = auth.decode_token(dltoken)['filename']
else:
as_attachment = False
base_path = session['folders'].get(root)
full_path = os.path.join(base_path or '', *relative_parts)
try: try:
full_path = check_path(full_path) full_path = check_path(full_path)
except (ValueError, PermissionError) as e: except (ValueError, PermissionError) as e:
return jsonify({'error': str(e)}), 403 return jsonify({'Unauthorized': str(e)}), 403
if not os.path.isfile(full_path): if not os.path.isfile(full_path):
app.logger.error(f"File not found: {full_path}") app.logger.error(f"File not found: {full_path}")
@ -465,9 +472,6 @@ def serve_file(subpath):
# 6) Build response for non-image # 6) Build response for non-image
filesize = os.path.getsize(file_path) filesize = os.path.getsize(file_path)
# Figure out download flag and filename
as_attachment = request.args.get('download') == 'true'
filename = os.path.basename(full_path) filename = os.path.basename(full_path)
if as_attachment: if as_attachment:
@ -483,8 +487,8 @@ def serve_file(subpath):
file_path, file_path,
mimetype=mimetype, mimetype=mimetype,
conditional=True, conditional=True,
as_attachment=as_attachment , as_attachment=as_attachment,
download_name=download_name download_name=filename if as_attachment else None
) )
if as_attachment: if as_attachment:
@ -526,9 +530,9 @@ def get_transcript(subpath):
return content, 200, {'Content-Type': 'text/markdown; charset=utf-8'} return content, 200, {'Content-Type': 'text/markdown; charset=utf-8'}
@app.route("/create_share/<path:subpath>") @app.route("/create_token/<path:subpath>")
@auth.require_secret @auth.require_secret
def create_share(subpath): def create_token(subpath):
scheme = request.scheme # current scheme (http or https) scheme = request.scheme # current scheme (http or https)
host = request.host host = request.host
if 'admin' not in session and not session.get('admin'): if 'admin' not in session and not session.get('admin'):
@ -556,7 +560,7 @@ def create_share(subpath):
] ]
} }
token = auth.generate_secret_key_compressed(data) token = auth.generate_token(data)
url = f"{scheme}://{host}?token={token}" url = f"{scheme}://{host}?token={token}"
qr = qrcode.QRCode(version=1, box_size=10, border=4) qr = qrcode.QRCode(version=1, box_size=10, border=4)
@ -567,7 +571,7 @@ def create_share(subpath):
img.save(buffer, format="PNG") img.save(buffer, format="PNG")
buffer.seek(0) buffer.seek(0)
img_base64 = base64.b64encode(buffer.getvalue()).decode('ascii') img_base64 = base64.b64encode(buffer.getvalue()).decode('ascii')
token_item = auth.decode_secret_key_compressed(token) token_item = auth.decode_token(token)
return render_template('view_token.html', return render_template('view_token.html',
token_qr_code=img_base64, token_qr_code=img_base64,
@ -577,6 +581,36 @@ def create_share(subpath):
) )
@app.route("/create_dltoken/<path:subpath>")
@auth.require_secret
def create_dltoken(subpath):
scheme = request.scheme # current scheme (http or https)
host = request.host
# 1) Locate the real file on disk
root, *relative_parts = subpath.split('/')
base_path = session['folders'].get(root)
full_path = os.path.join(base_path or '', *relative_parts)
try:
full_path = check_path(full_path)
except (ValueError, PermissionError) as e:
return jsonify({'Unauthorized': str(e)}), 403
if not os.path.isfile(full_path):
app.logger.error(f"File not found: {full_path}")
return "File not found", 404
validity_date = datetime.now().strftime('%d.%m.%Y')
data = {
"validity": validity_date,
"filename": str(full_path)
}
token = auth.generate_token(data)
url = f"{scheme}://{host}/media/{subpath}?dltoken={token}"
return url
def query_recent_connections(): def query_recent_connections():
global clients_connected, background_thread_running global clients_connected, background_thread_running
background_thread_running = True background_thread_running = True

24
auth.py
View File

@ -73,6 +73,12 @@ def require_secret(f):
# 1) Get secretand token from query params (if any) # 1) Get secretand token from query params (if any)
args_secret = request.args.get('secret') args_secret = request.args.get('secret')
args_token = request.args.get('token') args_token = request.args.get('token')
args_dltoken = request.args.get('dltoken')
# 1b) immediately return if dltoken is provided
if args_dltoken:
if is_valid_token(args_dltoken):
return f(*args, **kwargs)
# 2) Initialize 'valid_secrets' in the session if missing # 2) Initialize 'valid_secrets' in the session if missing
if 'valid_secrets' not in session: if 'valid_secrets' not in session:
@ -123,7 +129,7 @@ def require_secret(f):
session['folders'][folder_info['foldername']] = folder_info['folderpath'] session['folders'][folder_info['foldername']] = folder_info['folderpath']
for token_in_session in session.get('valid_tokens', []): for token_in_session in session.get('valid_tokens', []):
token_item = decode_secret_key_compressed(token_in_session) token_item = decode_token(token_in_session)
for folder_info in token_item['folders']: for folder_info in token_item['folders']:
session['folders'][folder_info['foldername']] = folder_info['folderpath'] session['folders'][folder_info['foldername']] = folder_info['folderpath']
@ -230,7 +236,7 @@ def mylinks():
img_base64 = base64.b64encode(buffer.getvalue()).decode('ascii') img_base64 = base64.b64encode(buffer.getvalue()).decode('ascii')
token_qr_codes[token] = img_base64 token_qr_codes[token] = img_base64
token_item = decode_secret_key_compressed(token) token_item = decode_token(token)
token_folders[token] = token_item.get('folders') token_folders[token] = token_item.get('folders')
token_url[token] = url token_url[token] = url
token_valid_to[token] = token_item.get('validity', 'Unbekannt') token_valid_to[token] = token_item.get('validity', 'Unbekannt')
@ -279,14 +285,14 @@ def is_valid_token(secret_key: str, hmac_length: int = 32) -> bool:
date has not yet passed. date has not yet passed.
Returns True if: Returns True if:
- signature matches (via decode_secret_key_compressed) - signature matches (via decode_token)
- the payload contains a "validity" field in DD.MM.YYYY format - the payload contains a "validity" field in DD.MM.YYYY format
- today's date <= validity date - today's date <= validity date
Otherwise returns False. Otherwise returns False.
""" """
try: try:
payload = decode_secret_key_compressed(secret_key, hmac_length) payload = decode_token(secret_key, hmac_length)
except ValueError: except ValueError:
return False return False
@ -326,7 +332,7 @@ def _shorten_keys(obj: Any, mapping: Dict[str,str]) -> Any:
else: else:
return obj return obj
def generate_secret_key_compressed( def generate_token(
payload: dict, payload: dict,
hmac_length: int = 32 hmac_length: int = 32
) -> str: ) -> str:
@ -364,12 +370,12 @@ def generate_secret_key_compressed(
return f"{encoded}{signature}{length_hex}" return f"{encoded}{signature}{length_hex}"
def decode_secret_key_compressed( def decode_token(
secret_key: str, secret_key: str,
hmac_length: int = 32 hmac_length: int = 32
) -> dict: ) -> dict:
""" """
Reverse of generate_secret_key_compressed: Reverse of generate_token:
- verify signature (using same hmac_length) - verify signature (using same hmac_length)
- base64-decode zlib-decompress JSON - base64-decode zlib-decompress JSON
- restore original keys - restore original keys
@ -431,8 +437,8 @@ if __name__ == '__main__':
] ]
} }
token = generate_secret_key_compressed(payload) token = generate_token(payload)
print("Token:", token) print("Token:", token)
result = decode_secret_key_compressed(token) result = decode_token(token)
print("Decoded payload:", result) print("Decoded payload:", result)

View File

@ -364,11 +364,11 @@ document.querySelectorAll('.play-file').forEach(link => {
}); });
}); });
// create share icon clicks. // create token icon clicks.
document.querySelectorAll('.create-share').forEach(link => { document.querySelectorAll('.create-share').forEach(link => {
link.addEventListener('click', function (event) { link.addEventListener('click', function (event) {
event.preventDefault(); event.preventDefault();
const url = '/create_share/' + this.getAttribute('data-url'); const url = '/create_token/' + this.getAttribute('data-url');
console.log(url); console.log(url);
fetch(url) fetch(url)
.then(response => { .then(response => {

View File

@ -139,18 +139,30 @@ class SimpleAudioPlayer {
} }
async fileDownload() { async fileDownload() {
const src = this.audio.currentSrc || this.audio.src; const src = this.audio.currentSrc || this.audio.src;
if (!src) return; if (!src) return;
// Build the URL with your download flag + cachebuster // Extract the subpath from the src
const downloadUrl = new URL(src, window.location.href); const urlObj = new URL(src, window.location.href);
downloadUrl.searchParams.set('download', 'true'); let subpath = urlObj.pathname;
downloadUrl.searchParams.set('_', Date.now()); subpath = subpath.slice('/media'.length);
// Create a “real” link to that URL and click it // Fetch the tokenized URL from your backend
let tokenizedUrl;
try {
const resp = await fetch(`/create_dltoken${subpath}`);
if (!resp.ok) return;
tokenizedUrl = await resp.text();
} catch {
return;
}
// Build the URL with cache-buster
const downloadUrl = new URL(tokenizedUrl, window.location.href);
// Create a link and click it
const a = document.createElement('a'); const a = document.createElement('a');
a.href = downloadUrl.toString(); a.href = downloadUrl.toString();
// NOTE: do NOT set a.download here we want the server's Content-Disposition to drive it
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);