Passwords for files
This commit is contained in:
50
code/app.py
50
code/app.py
@@ -8,13 +8,14 @@ from flask import (
|
|||||||
render_template,
|
render_template,
|
||||||
jsonify,
|
jsonify,
|
||||||
request,
|
request,
|
||||||
|
url_for,
|
||||||
|
redirect,
|
||||||
send_from_directory,
|
send_from_directory,
|
||||||
|
session,
|
||||||
)
|
)
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from revprox import ReverseProxied
|
from revprox import ReverseProxied
|
||||||
from utils.misc import (
|
from utils.misc import random_token, hash_password, verify_password
|
||||||
random_token,
|
|
||||||
)
|
|
||||||
from utils.files import (
|
from utils.files import (
|
||||||
db_store_file,
|
db_store_file,
|
||||||
file_details,
|
file_details,
|
||||||
@@ -53,6 +54,7 @@ def upload():
|
|||||||
-H "Name: my.file.ext" \
|
-H "Name: my.file.ext" \
|
||||||
-H "Max-Downloads: 4000" \
|
-H "Max-Downloads: 4000" \
|
||||||
-H "Expires-Days: 14" \
|
-H "Expires-Days: 14" \
|
||||||
|
-H "Password: mypass" \
|
||||||
-H "Secret: dff789f0bbe8183d32542" \
|
-H "Secret: dff789f0bbe8183d32542" \
|
||||||
"$FLASK_PUBLIC_URL"/upload
|
"$FLASK_PUBLIC_URL"/upload
|
||||||
|
|
||||||
@@ -87,12 +89,17 @@ def upload():
|
|||||||
)
|
)
|
||||||
if "Expires-hours" in request.headers:
|
if "Expires-hours" in request.headers:
|
||||||
expires = int(time.time()) + 3600 * int(request.headers.get("Expires-hours"))
|
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:
|
while True:
|
||||||
token = random_token()
|
token = random_token()
|
||||||
folder = os.path.join(app.config["DATAFOLDER"], token)
|
folder = os.path.join(app.config["DATAFOLDER"], token)
|
||||||
if not os.path.exists(folder):
|
if not os.path.exists(folder):
|
||||||
break
|
break
|
||||||
|
|
||||||
filename = file_full_path(token, safe_filename)
|
filename = file_full_path(token, safe_filename)
|
||||||
os.mkdir(folder)
|
os.mkdir(folder)
|
||||||
|
|
||||||
@@ -112,7 +119,7 @@ def upload():
|
|||||||
break
|
break
|
||||||
f.write(chunk)
|
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)
|
download_url = file_full_url(token, safe_filename)
|
||||||
return "File uploaded\n%s\n" % (download_url,), 200
|
return "File uploaded\n%s\n" % (download_url,), 200
|
||||||
|
|
||||||
@@ -190,6 +197,10 @@ def download(name, token):
|
|||||||
"""
|
"""
|
||||||
Download a file
|
Download a file
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if "Password" in request.headers:
|
||||||
|
session[token] = request.headers["Password"]
|
||||||
|
|
||||||
return download_file(token, name)
|
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):
|
def download_file(token, name):
|
||||||
"""
|
"""
|
||||||
check for file expiry, and send file if allowed
|
check for file expiry, and send file if allowed
|
||||||
@@ -215,13 +246,22 @@ def download_file(token, name):
|
|||||||
return "Error", 404
|
return "Error", 404
|
||||||
db_stat = db_get_file(token, name)
|
db_stat = db_get_file(token, name)
|
||||||
if db_stat:
|
if db_stat:
|
||||||
added, expires, downloads, max_dl = db_stat
|
added, expires, downloads, max_dl, password_hash = db_stat
|
||||||
else:
|
else:
|
||||||
return "Error", 404
|
return "Error", 404
|
||||||
if downloads >= max_dl and max_dl > -1:
|
if downloads >= max_dl and max_dl > -1:
|
||||||
return "Expired", 401
|
return "Expired", 401
|
||||||
if expires < time.time():
|
if expires < time.time():
|
||||||
return "Expired", 401
|
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)
|
db_add_download(token, name)
|
||||||
return send_from_directory(
|
return send_from_directory(
|
||||||
directory=os.path.join(app.config["DATAFOLDER"], token), path=name
|
directory=os.path.join(app.config["DATAFOLDER"], token), path=name
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ CREATE TABLE IF NOT EXISTS files (
|
|||||||
added integer NOT NULL,
|
added integer NOT NULL,
|
||||||
expires integer NOT NULL,
|
expires integer NOT NULL,
|
||||||
downloads integer NOT NULL,
|
downloads integer NOT NULL,
|
||||||
max_downloads integer NOT NULL
|
max_downloads integer NOT NULL,
|
||||||
|
passhash text
|
||||||
);
|
);
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
flask
|
flask
|
||||||
gunicorn
|
gunicorn
|
||||||
|
passlib
|
||||||
|
argon2_cffi
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ _help() {
|
|||||||
|
|
||||||
-d max days to keep (default 30)
|
-d max days to keep (default 30)
|
||||||
-m max downloads to keep (default 9999, -1 to disable)
|
-m max downloads to keep (default 9999, -1 to disable)
|
||||||
|
-p password
|
||||||
|
|
||||||
Filename:
|
Filename:
|
||||||
When writing to share:
|
When writing to share:
|
||||||
@@ -105,6 +106,7 @@ _write_folder() { # name, file
|
|||||||
-H "Max-Downloads: $MAXDL" \
|
-H "Max-Downloads: $MAXDL" \
|
||||||
-H "Expires-Days: $MAXDAYS" \
|
-H "Expires-Days: $MAXDAYS" \
|
||||||
-H "Secret: $MFL_TOKEN" \
|
-H "Secret: $MFL_TOKEN" \
|
||||||
|
-H "Password: $PASSWORD" \
|
||||||
"$MFL_ROOTURL"/upload | cat
|
"$MFL_ROOTURL"/upload | cat
|
||||||
}
|
}
|
||||||
_write_file() { # name, file
|
_write_file() { # name, file
|
||||||
@@ -113,6 +115,7 @@ _write_file() { # name, file
|
|||||||
-H "Max-Downloads: $MAXDL" \
|
-H "Max-Downloads: $MAXDL" \
|
||||||
-H "Expires-Days: $MAXDAYS" \
|
-H "Expires-Days: $MAXDAYS" \
|
||||||
-H "Secret: $MFL_TOKEN" \
|
-H "Secret: $MFL_TOKEN" \
|
||||||
|
-H "Password: $PASSWORD" \
|
||||||
"$MFL_ROOTURL"/upload | cat
|
"$MFL_ROOTURL"/upload | cat
|
||||||
}
|
}
|
||||||
_write_stdin() { # name
|
_write_stdin() { # name
|
||||||
@@ -122,6 +125,7 @@ _write_stdin() { # name
|
|||||||
-H "Max-Downloads: $MAXDL" \
|
-H "Max-Downloads: $MAXDL" \
|
||||||
-H "Expires-Days: $MAXDAYS" \
|
-H "Expires-Days: $MAXDAYS" \
|
||||||
-H "Secret: $MFL_TOKEN" \
|
-H "Secret: $MFL_TOKEN" \
|
||||||
|
-H "Password: $PASSWORD" \
|
||||||
"$MFL_ROOTURL"/upload | cat
|
"$MFL_ROOTURL"/upload | cat
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +204,7 @@ for (( i=2; i<=$#; i++ )); do
|
|||||||
j=$(( i + 1 ))
|
j=$(( i + 1 ))
|
||||||
[[ "${!i}" = "-m" ]] && { MAXDL=${!j}; i=$j; continue; }
|
[[ "${!i}" = "-m" ]] && { MAXDL=${!j}; i=$j; continue; }
|
||||||
[[ "${!i}" = "-d" ]] && { MAXDAYS=${!j}; i=$j; continue; }
|
[[ "${!i}" = "-d" ]] && { MAXDAYS=${!j}; i=$j; continue; }
|
||||||
|
[[ "${!i}" = "-p" ]] && { PASSWORD=${!j}; i=$j; continue; }
|
||||||
if [[ -z "$FILE" ]]; then
|
if [[ -z "$FILE" ]]; then
|
||||||
FILE="${!i}"
|
FILE="${!i}"
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ def get_db():
|
|||||||
return db, c
|
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()
|
db, c = get_db()
|
||||||
c.execute(
|
c.execute(
|
||||||
"""
|
"""
|
||||||
insert into files(token,name,added,expires,downloads,max_downloads)
|
insert into files(token,name,added,expires,downloads,max_downloads,passhash)
|
||||||
values (?,?,?,?,?,?)
|
values (?,?,?,?,?,?,?)
|
||||||
""",
|
""",
|
||||||
(token, name, int(time.time()), expires, 0, max_dl),
|
(token, name, int(time.time()), expires, 0, max_dl, password),
|
||||||
)
|
)
|
||||||
if c.rowcount > 0:
|
if c.rowcount > 0:
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -31,7 +31,7 @@ def db_get_file(token, name):
|
|||||||
db, c = get_db()
|
db, c = get_db()
|
||||||
return db.execute(
|
return db.execute(
|
||||||
"""
|
"""
|
||||||
SELECT added,expires,downloads,max_downloads
|
SELECT added,expires,downloads,max_downloads,passhash
|
||||||
FROM files WHERE token = ? AND name = ?
|
FROM files WHERE token = ? AND name = ?
|
||||||
""",
|
""",
|
||||||
(token, name),
|
(token, name),
|
||||||
@@ -42,7 +42,7 @@ def db_get_files():
|
|||||||
db, c = get_db()
|
db, c = get_db()
|
||||||
return db.execute(
|
return db.execute(
|
||||||
"""
|
"""
|
||||||
SELECT token,name,added,expires,downloads,max_downloads
|
SELECT token,name,added,expires,downloads,max_downloads,passhash
|
||||||
FROM files
|
FROM files
|
||||||
WHERE expires > ? and (downloads < max_downloads or max_downloads = -1)
|
WHERE expires > ? and (downloads < max_downloads or max_downloads = -1)
|
||||||
ORDER BY added
|
ORDER BY added
|
||||||
@@ -157,7 +157,7 @@ def file_details(token, name):
|
|||||||
s = os.stat(full_path)
|
s = os.stat(full_path)
|
||||||
db_stat = db_get_file(token, name)
|
db_stat = db_get_file(token, name)
|
||||||
if db_stat:
|
if db_stat:
|
||||||
added, expires, downloads, max_dl = db_stat
|
added, expires, downloads, max_dl, password = db_stat
|
||||||
else:
|
else:
|
||||||
return {}
|
return {}
|
||||||
return {
|
return {
|
||||||
@@ -169,6 +169,7 @@ def file_details(token, name):
|
|||||||
"expires": file_time_human(expires),
|
"expires": file_time_human(expires),
|
||||||
"downloaded": downloads,
|
"downloaded": downloads,
|
||||||
"max-dl": max_dl,
|
"max-dl": max_dl,
|
||||||
|
"protected": password is not None,
|
||||||
}
|
}
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return {}
|
return {}
|
||||||
@@ -190,7 +191,8 @@ def file_list():
|
|||||||
url = file_full_url(file[0], file[1])
|
url = file_full_url(file[0], file[1])
|
||||||
added = file_date_human(file[2])
|
added = file_date_human(file[2])
|
||||||
expiry = file_date_human(file[3])
|
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
|
return details
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import secrets
|
import secrets
|
||||||
import string
|
import string
|
||||||
|
import passlib.hash
|
||||||
|
|
||||||
VALID_TOKEN_CHARS = string.digits + string.ascii_letters
|
VALID_TOKEN_CHARS = string.digits + string.ascii_letters
|
||||||
|
|
||||||
@@ -29,3 +30,11 @@ def file_size_human(num, HTML=True):
|
|||||||
|
|
||||||
def file_size_MB(num):
|
def file_size_MB(num):
|
||||||
return "{:,.2f}".format(num / (1024 * 1024))
|
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)
|
||||||
|
|||||||
@@ -192,8 +192,6 @@ function t08-file-details() {
|
|||||||
"$FLASK_PUBLIC_URL"/details/"$token_name"
|
"$FLASK_PUBLIC_URL"/details/"$token_name"
|
||||||
}
|
}
|
||||||
function t09-file-delete() {
|
function t09-file-delete() {
|
||||||
|
|
||||||
|
|
||||||
download_url=$( curl -fL -w "\n" \
|
download_url=$( curl -fL -w "\n" \
|
||||||
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
-H "Secret: $FLASK_ACCESS_TOKEN" \
|
||||||
"$FLASK_PUBLIC_URL"/ls | grep "$BIGS" | tail -n 1 | sed s,.*http,http, )
|
"$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
|
cat mfl | ./mfl w mfl
|
||||||
./mfl w . "folder with spaces"
|
./mfl w . "folder with spaces"
|
||||||
./mfl w "$SMALL"
|
./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() {
|
_getlist() {
|
||||||
|
|||||||
Reference in New Issue
Block a user