From 3b69ce7ae1a19de629744bd4bbd3789bbb721e24 Mon Sep 17 00:00:00 2001 From: Q Date: Mon, 28 Aug 2023 22:13:22 +0300 Subject: [PATCH] shareable upload tokens --- code/app.py | 37 +++++++++++++++++++++++++-- code/init_db.sh | 8 ++++++ code/templates/mfl | 27 +++++++++++++++++--- code/utils/files.py | 62 ++++++++++++++++++++++++++++++++++++++++++++- code/utils/misc.py | 4 +-- test/run-tests.sh | 15 +++++++++++ 6 files changed, 145 insertions(+), 8 deletions(-) diff --git a/code/app.py b/code/app.py index 63e92ad..859c447 100644 --- a/code/app.py +++ b/code/app.py @@ -27,6 +27,9 @@ from utils.files import ( db_get_file, db_delete_file, db_maintenance, + validate_upload_token, + invalidate_upload_token, + new_upload_token, ) import logging @@ -97,8 +100,13 @@ def upload(): return "Name required", 500 safe_filename = secure_filename(name) secret = request.headers.get("Secret", "") - if secret != app.config["ACCESS_TOKEN"]: - return "Error", 401 + upload_token = request.headers.get("Token", False) + if upload_token: + if not validate_upload_token(upload_token): + return "Error", 401 + else: + if secret != app.config["ACCESS_TOKEN"]: + return "Error", 401 max_dl = request.headers.get("Max-Downloads", app.config["DEFAULT_MAX_DL"]) expires = int(time.time()) + int(app.config["DEFAULT_EXPIRE"]) if "Expires-days" in request.headers: @@ -142,9 +150,34 @@ def upload(): app.logger.info( f"Upload: {download_url} MaxDL:{max_dl} Exp:{file_date_human(expires)}" ) + if upload_token: + invalidate_upload_token(upload_token) return "File uploaded\n%s\n" % (download_url,), 200 +@app.route("/new_token", methods=["GET"]) +def upload_token(): + """ + Get JSON of file details. Size, added date, download times, etc. + + curl -fL -w "\n" \ + -H "Expires-Days: 14" \ + -H "Secret: dff789f0bbe8183d32542" \ + "$FLASK_PUBLIC_URL"/new_token + + """ + secret = request.headers.get("Secret", "") + if secret != app.config["ACCESS_TOKEN"]: + return "Error", 401 + expires = int(time.time()) + int(app.config["DEFAULT_EXPIRE"]) + if "Expires-days" in request.headers: + expires = int(time.time()) + 24 * 3600 * int( + request.headers.get("Expires-days") + ) + token = new_upload_token(expires) + return token, 200 + + @app.route("/details//", methods=["GET"]) def details(token, name): """ diff --git a/code/init_db.sh b/code/init_db.sh index 66d4e46..467839d 100644 --- a/code/init_db.sh +++ b/code/init_db.sh @@ -12,3 +12,11 @@ CREATE TABLE IF NOT EXISTS files ( passhash text ); EOF + +cat <<'EOF' | sqlite3 "$1" +CREATE TABLE IF NOT EXISTS upload_tokens ( + token text PRIMARY KEY, + added integer NOT NULL, + expires integer NOT NULL +); +EOF diff --git a/code/templates/mfl b/code/templates/mfl index 247f739..5b89321 100755 --- a/code/templates/mfl +++ b/code/templates/mfl @@ -10,7 +10,7 @@ _help() { (w)rite Save to share, read from stdin/file/folder (d)elete Delete an entry show Show details on file - upload Get URL for uploads [no arguments] + upload_token Get URL for uploads ( -d expiry ) autocomplete Get Bash autocompletion script update Update self @@ -154,6 +154,27 @@ _maintain() { "$MFL_ROOTURL"/maintenance } +_upload_token() { + token=$( curl -fL -s \ + -H "Expires-Days: $MAXDAYS" \ + -H "Secret: $MFL_TOKEN" \ + "$MFL_ROOTURL"/new_token ) + cat <&2 @@ -166,7 +187,7 @@ _get_completer() { local curr_arg curr_arg=${COMP_WORDS[COMP_CWORD]} if [[ $COMP_CWORD -eq 1 ]]; then - COMPREPLY=( $(compgen -W "help autocomplete list write delete show update" -- $curr_arg ) ); + COMPREPLY=( $(compgen -W "help autocomplete list write delete show update upload_token" -- $curr_arg ) ); fi if [[ $COMP_CWORD -eq 2 ]]; then case ${COMP_WORDS[$(( $COMP_CWORD - 1 ))]} in @@ -227,7 +248,7 @@ fi [[ "$1" = "simplelist" ]] && { CMD=simple_list; ARG1=$CMD; } [[ "$1" = "autocomplete" ]] && { _get_completer; } [[ "$1" = "update" ]] && { _update_client; } -[[ "$1" = "upload" ]] && { _upload_url; } +[[ "$1" = "upload_token" ]] && { _upload_token; exit; } [[ "$1" = "self" ]] && { _self_url; } [[ "$1" = "h" || "$1" = "help" ]] && CMD=help diff --git a/code/utils/files.py b/code/utils/files.py index 947b614..48d2e2b 100644 --- a/code/utils/files.py +++ b/code/utils/files.py @@ -4,7 +4,7 @@ from flask import current_app as app import time import sqlite3 import shutil -from .misc import file_size_human, file_date_human, file_time_human +from .misc import file_size_human, file_date_human, file_time_human, random_token def get_db(): @@ -136,6 +136,28 @@ def db_maintenance(): c.executemany("DELETE FROM files WHERE token = ?", deleted_tokens) if c.rowcount > 0: db.commit() + + # === Delete upload token entries where expiry is used up === + db, c = get_db() + rows = db.execute( + """ + select + token + from upload_tokens + where expires < ? + """, + (int(time.time()),), + ) + deleted_tokens = [] + for row in rows: + deleted_tokens.append((row[0],)) + messages.append(f"Deleting upload_token {row[0]}") + if len(deleted_tokens) > 0: + db, c = get_db() + c.executemany("DELETE FROM upload_tokens WHERE token = ?", deleted_tokens) + if c.rowcount > 0: + db.commit() + messages.append("Maintenance done.") return "\n".join(messages) @@ -199,3 +221,41 @@ def file_list(): def file_list_simple(): return [f"{r[0]}/{r[1]}" for r in db_get_files()] + + +def new_upload_token(expires): + db, c = get_db() + token = random_token(32) + c.execute( + """ + insert into upload_tokens(token,added,expires) + values (?,?,?) + """, + (token, int(time.time()), expires), + ) + if c.rowcount > 0: + db.commit() + return token + + +def validate_upload_token(token): + db, c = get_db() + return db.execute( + """ + SELECT 1 + FROM upload_tokens WHERE token = ? AND expires > ? + """, + (token, int(time.time())), + ).fetchone() + + +def invalidate_upload_token(token): + db, c = get_db() + c.execute( + """ + DELETE FROM upload_tokens WHERE token = ? + """, + (token,), + ) + if c.rowcount > 0: + db.commit() diff --git a/code/utils/misc.py b/code/utils/misc.py index da98b61..66c23af 100644 --- a/code/utils/misc.py +++ b/code/utils/misc.py @@ -6,8 +6,8 @@ import passlib.hash VALID_TOKEN_CHARS = string.digits + string.ascii_letters -def random_token(): - return "".join(secrets.choice(VALID_TOKEN_CHARS) for i in range(8)) +def random_token(n=8): + return "".join(secrets.choice(VALID_TOKEN_CHARS) for i in range(n)) def file_date_human(num): diff --git a/test/run-tests.sh b/test/run-tests.sh index 3754db2..cf94f46 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -298,11 +298,26 @@ function t16-download-password() { -H "Password: wrongpassword" \ "$FLASK_PUBLIC_URL"/d/"$token_name" > downloaded file downloaded +} + +function t17-new-upload_token() { + token=$( curl -fL -w "\n" \ + -H "Secret: $FLASK_ACCESS_TOKEN" \ + -H "Expiry-days: 1" \ + "$FLASK_PUBLIC_URL"/new_token ) + echo $token + cat "$IMAGE" | \ + curl -fL -w "\n" -F file="@-" -X POST \ + -H "Name: $IMAGE" \ + -H "Token: $token" \ + "$FLASK_PUBLIC_URL"/upload + } + _getlist() { declare -F | awk '{ print $3 }' | grep -v ^_ echo exit