Compare commits
3 Commits
0025f887df
...
bd3d7509b2
| Author | SHA1 | Date | |
|---|---|---|---|
| bd3d7509b2 | |||
| cb3d8efd72 | |||
| f96f4a40a5 |
60
app.py
60
app.py
@ -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
24
auth.py
@ -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)
|
||||||
@ -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 => {
|
||||||
|
|||||||
@ -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 + cache‑buster
|
// 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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user