diff --git a/code/app.py b/code/app.py index d9335e4..650f05d 100644 --- a/code/app.py +++ b/code/app.py @@ -8,13 +8,14 @@ from flask import ( render_template, jsonify, request, + url_for, + redirect, send_from_directory, + session, ) from werkzeug.utils import secure_filename from revprox import ReverseProxied -from utils.misc import ( - random_token, -) +from utils.misc import random_token, hash_password, verify_password from utils.files import ( db_store_file, file_details, @@ -53,6 +54,7 @@ def upload(): -H "Name: my.file.ext" \ -H "Max-Downloads: 4000" \ -H "Expires-Days: 14" \ + -H "Password: mypass" \ -H "Secret: dff789f0bbe8183d32542" \ "$FLASK_PUBLIC_URL"/upload @@ -87,12 +89,17 @@ def upload(): ) if "Expires-hours" in request.headers: expires = int(time.time()) + 3600 * int(request.headers.get("Expires-hours")) + password = None + if "Password" in request.headers: + if request.headers["Password"] != "": + password = hash_password(request.headers["Password"]) while True: token = random_token() folder = os.path.join(app.config["DATAFOLDER"], token) if not os.path.exists(folder): break + filename = file_full_path(token, safe_filename) os.mkdir(folder) @@ -112,7 +119,7 @@ def upload(): break f.write(chunk) - db_store_file(token, safe_filename, expires, max_dl) + db_store_file(token, safe_filename, expires, max_dl, password) download_url = file_full_url(token, safe_filename) return "File uploaded\n%s\n" % (download_url,), 200 @@ -190,6 +197,10 @@ def download(name, token): """ Download a file """ + + if "Password" in request.headers: + session[token] = request.headers["Password"] + return download_file(token, name) @@ -206,6 +217,26 @@ def script_mfl(): ) +@app.route("/login", methods=["GET", "POST"]) +def login(): + if request.method == "POST": + session[request.form["token"]] = request.form["password"] + return redirect(request.form["redirect"]) + + return render_template( + "login.html", + filename=session["name"], + redirect=session["redirect"], + token=session["token"], + ) + + +@app.route("/logout", methods=["GET"]) +def logout(): + session.clear() + return "OK", 200 + + def download_file(token, name): """ check for file expiry, and send file if allowed @@ -215,13 +246,22 @@ def download_file(token, name): return "Error", 404 db_stat = db_get_file(token, name) if db_stat: - added, expires, downloads, max_dl = db_stat + added, expires, downloads, max_dl, password_hash = db_stat else: return "Error", 404 if downloads >= max_dl and max_dl > -1: return "Expired", 401 if expires < time.time(): return "Expired", 401 + if password_hash: + if verify_password(session.get(token, ""), password_hash): + pass + else: + session["token"] = token + session["name"] = name + session["redirect"] = url_for("download", name=name, token=token) + return redirect(url_for("login")) + db_add_download(token, name) return send_from_directory( directory=os.path.join(app.config["DATAFOLDER"], token), path=name diff --git a/code/init_db.sh b/code/init_db.sh index 8b66414..66d4e46 100644 --- a/code/init_db.sh +++ b/code/init_db.sh @@ -8,6 +8,7 @@ CREATE TABLE IF NOT EXISTS files ( added integer NOT NULL, expires integer NOT NULL, downloads integer NOT NULL, - max_downloads integer NOT NULL + max_downloads integer NOT NULL, + passhash text ); EOF diff --git a/code/requirements.txt b/code/requirements.txt index e4a286c..21aa056 100644 --- a/code/requirements.txt +++ b/code/requirements.txt @@ -1,2 +1,4 @@ flask gunicorn +passlib +argon2_cffi diff --git a/code/templates/mfl b/code/templates/mfl index 60411d8..247f739 100755 --- a/code/templates/mfl +++ b/code/templates/mfl @@ -16,6 +16,7 @@ _help() { -d max days to keep (default 30) -m max downloads to keep (default 9999, -1 to disable) + -p password Filename: When writing to share: @@ -105,6 +106,7 @@ _write_folder() { # name, file -H "Max-Downloads: $MAXDL" \ -H "Expires-Days: $MAXDAYS" \ -H "Secret: $MFL_TOKEN" \ + -H "Password: $PASSWORD" \ "$MFL_ROOTURL"/upload | cat } _write_file() { # name, file @@ -113,6 +115,7 @@ _write_file() { # name, file -H "Max-Downloads: $MAXDL" \ -H "Expires-Days: $MAXDAYS" \ -H "Secret: $MFL_TOKEN" \ + -H "Password: $PASSWORD" \ "$MFL_ROOTURL"/upload | cat } _write_stdin() { # name @@ -122,6 +125,7 @@ _write_stdin() { # name -H "Max-Downloads: $MAXDL" \ -H "Expires-Days: $MAXDAYS" \ -H "Secret: $MFL_TOKEN" \ + -H "Password: $PASSWORD" \ "$MFL_ROOTURL"/upload | cat } @@ -200,6 +204,7 @@ for (( i=2; i<=$#; i++ )); do j=$(( i + 1 )) [[ "${!i}" = "-m" ]] && { MAXDL=${!j}; i=$j; continue; } [[ "${!i}" = "-d" ]] && { MAXDAYS=${!j}; i=$j; continue; } + [[ "${!i}" = "-p" ]] && { PASSWORD=${!j}; i=$j; continue; } if [[ -z "$FILE" ]]; then FILE="${!i}" continue diff --git a/code/utils/files.py b/code/utils/files.py index f775279..947b614 100644 --- a/code/utils/files.py +++ b/code/utils/files.py @@ -13,14 +13,14 @@ def get_db(): return db, c -def db_store_file(token, name, expires, max_dl): +def db_store_file(token, name, expires, max_dl, password): db, c = get_db() c.execute( """ - insert into files(token,name,added,expires,downloads,max_downloads) - values (?,?,?,?,?,?) + insert into files(token,name,added,expires,downloads,max_downloads,passhash) + values (?,?,?,?,?,?,?) """, - (token, name, int(time.time()), expires, 0, max_dl), + (token, name, int(time.time()), expires, 0, max_dl, password), ) if c.rowcount > 0: db.commit() @@ -31,7 +31,7 @@ def db_get_file(token, name): db, c = get_db() return db.execute( """ - SELECT added,expires,downloads,max_downloads + SELECT added,expires,downloads,max_downloads,passhash FROM files WHERE token = ? AND name = ? """, (token, name), @@ -42,7 +42,7 @@ def db_get_files(): db, c = get_db() return db.execute( """ - SELECT token,name,added,expires,downloads,max_downloads + SELECT token,name,added,expires,downloads,max_downloads,passhash FROM files WHERE expires > ? and (downloads < max_downloads or max_downloads = -1) ORDER BY added @@ -157,7 +157,7 @@ def file_details(token, name): s = os.stat(full_path) db_stat = db_get_file(token, name) if db_stat: - added, expires, downloads, max_dl = db_stat + added, expires, downloads, max_dl, password = db_stat else: return {} return { @@ -169,6 +169,7 @@ def file_details(token, name): "expires": file_time_human(expires), "downloaded": downloads, "max-dl": max_dl, + "protected": password is not None, } except FileNotFoundError: return {} @@ -190,7 +191,8 @@ def file_list(): url = file_full_url(file[0], file[1]) added = file_date_human(file[2]) expiry = file_date_human(file[3]) - details.append(f"{added}/{expiry} {file[4]:4d}/{file[5]:4d} {url}") + pw = " (PW)" if file[6] else "" + details.append(f"{added}/{expiry} {file[4]:4d}/{file[5]:4d} {url}{pw}") return details diff --git a/code/utils/misc.py b/code/utils/misc.py index 2cc5062..da98b61 100644 --- a/code/utils/misc.py +++ b/code/utils/misc.py @@ -1,6 +1,7 @@ from datetime import datetime import secrets import string +import passlib.hash VALID_TOKEN_CHARS = string.digits + string.ascii_letters @@ -29,3 +30,11 @@ def file_size_human(num, HTML=True): def file_size_MB(num): return "{:,.2f}".format(num / (1024 * 1024)) + + +def hash_password(password): + return passlib.hash.argon2.hash(password) + + +def verify_password(password, hash): + return passlib.hash.argon2.verify(password, hash) diff --git a/test/run-tests.sh b/test/run-tests.sh index 29be7ba..3754db2 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -192,8 +192,6 @@ function t08-file-details() { "$FLASK_PUBLIC_URL"/details/"$token_name" } function t09-file-delete() { - - download_url=$( curl -fL -w "\n" \ -H "Secret: $FLASK_ACCESS_TOKEN" \ "$FLASK_PUBLIC_URL"/ls | grep "$BIGS" | tail -n 1 | sed s,.*http,http, ) @@ -271,8 +269,38 @@ function t14-mfl-upload() { cat mfl | ./mfl w mfl ./mfl w . "folder with spaces" ./mfl w "$SMALL" + ./mfl w -p "passwordprotected" "$IMAGE" } +function t15-upload-password() { + cat "$IMAGE" | \ + curl -fL -w "\n" -F file="@-" -X POST \ + -H "Name: $IMAGE" \ + -H "Password: password" \ + -H "Secret: $FLASK_ACCESS_TOKEN" \ + "$FLASK_PUBLIC_URL"/upload + +} + +function t16-download-password() { + download_url=$( curl -fL -w "\n" \ + -H "Secret: $FLASK_ACCESS_TOKEN" \ + "$FLASK_PUBLIC_URL"/ls | grep "$IMAGE" | tail -n 1 | sed s,.*http,http, ) + token_name=$( echo $download_url | sed s,.*/d/,, ) + curl -fL -w "\n" \ + -H "Secret: $FLASK_ACCESS_TOKEN" \ + -H "Password: password" \ + "$FLASK_PUBLIC_URL"/d/"$token_name" > downloaded + file downloaded + + curl -fL -w "\n" \ + -H "Secret: $FLASK_ACCESS_TOKEN" \ + -H "Password: wrongpassword" \ + "$FLASK_PUBLIC_URL"/d/"$token_name" > downloaded + file downloaded + + +} _getlist() {