shareable upload tokens
This commit is contained in:
33
code/app.py
33
code/app.py
@@ -27,6 +27,9 @@ from utils.files import (
|
|||||||
db_get_file,
|
db_get_file,
|
||||||
db_delete_file,
|
db_delete_file,
|
||||||
db_maintenance,
|
db_maintenance,
|
||||||
|
validate_upload_token,
|
||||||
|
invalidate_upload_token,
|
||||||
|
new_upload_token,
|
||||||
)
|
)
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -97,6 +100,11 @@ def upload():
|
|||||||
return "Name required", 500
|
return "Name required", 500
|
||||||
safe_filename = secure_filename(name)
|
safe_filename = secure_filename(name)
|
||||||
secret = request.headers.get("Secret", "")
|
secret = request.headers.get("Secret", "")
|
||||||
|
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"]:
|
if secret != app.config["ACCESS_TOKEN"]:
|
||||||
return "Error", 401
|
return "Error", 401
|
||||||
max_dl = request.headers.get("Max-Downloads", app.config["DEFAULT_MAX_DL"])
|
max_dl = request.headers.get("Max-Downloads", app.config["DEFAULT_MAX_DL"])
|
||||||
@@ -142,9 +150,34 @@ def upload():
|
|||||||
app.logger.info(
|
app.logger.info(
|
||||||
f"Upload: {download_url} MaxDL:{max_dl} Exp:{file_date_human(expires)}"
|
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
|
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/<token>/<name>", methods=["GET"])
|
@app.route("/details/<token>/<name>", methods=["GET"])
|
||||||
def details(token, name):
|
def details(token, name):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -12,3 +12,11 @@ CREATE TABLE IF NOT EXISTS files (
|
|||||||
passhash text
|
passhash text
|
||||||
);
|
);
|
||||||
EOF
|
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
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ _help() {
|
|||||||
(w)rite Save to share, read from stdin/file/folder
|
(w)rite Save to share, read from stdin/file/folder
|
||||||
(d)elete Delete an entry
|
(d)elete Delete an entry
|
||||||
show Show details on file
|
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
|
autocomplete Get Bash autocompletion script
|
||||||
update Update self
|
update Update self
|
||||||
|
|
||||||
@@ -154,6 +154,27 @@ _maintain() {
|
|||||||
"$MFL_ROOTURL"/maintenance
|
"$MFL_ROOTURL"/maintenance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_upload_token() {
|
||||||
|
token=$( curl -fL -s \
|
||||||
|
-H "Expires-Days: $MAXDAYS" \
|
||||||
|
-H "Secret: $MFL_TOKEN" \
|
||||||
|
"$MFL_ROOTURL"/new_token )
|
||||||
|
cat <<EOF
|
||||||
|
curl -fL -g --upload-file "file_to_upload.ext" \\
|
||||||
|
-H "Name: name_to_store_as.ext" \\
|
||||||
|
-H "Max-Downloads: 10" \\
|
||||||
|
-H "Expires-Days: 10" \\
|
||||||
|
-H "Token: $token" \\
|
||||||
|
-H "Password: lock_with_password" \\
|
||||||
|
"$MFL_ROOTURL"/upload
|
||||||
|
|
||||||
|
curl -fL -g --upload-file "file_to_upload.ext" \\
|
||||||
|
-H "Name: name_to_store_as.ext" \\
|
||||||
|
-H "Token: $token" \\
|
||||||
|
"$MFL_ROOTURL"/upload
|
||||||
|
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
_msg() {
|
_msg() {
|
||||||
echo "$@" >&2
|
echo "$@" >&2
|
||||||
@@ -166,7 +187,7 @@ _get_completer() {
|
|||||||
local curr_arg
|
local curr_arg
|
||||||
curr_arg=${COMP_WORDS[COMP_CWORD]}
|
curr_arg=${COMP_WORDS[COMP_CWORD]}
|
||||||
if [[ $COMP_CWORD -eq 1 ]]; then
|
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
|
fi
|
||||||
if [[ $COMP_CWORD -eq 2 ]]; then
|
if [[ $COMP_CWORD -eq 2 ]]; then
|
||||||
case ${COMP_WORDS[$(( $COMP_CWORD - 1 ))]} in
|
case ${COMP_WORDS[$(( $COMP_CWORD - 1 ))]} in
|
||||||
@@ -227,7 +248,7 @@ fi
|
|||||||
[[ "$1" = "simplelist" ]] && { CMD=simple_list; ARG1=$CMD; }
|
[[ "$1" = "simplelist" ]] && { CMD=simple_list; ARG1=$CMD; }
|
||||||
[[ "$1" = "autocomplete" ]] && { _get_completer; }
|
[[ "$1" = "autocomplete" ]] && { _get_completer; }
|
||||||
[[ "$1" = "update" ]] && { _update_client; }
|
[[ "$1" = "update" ]] && { _update_client; }
|
||||||
[[ "$1" = "upload" ]] && { _upload_url; }
|
[[ "$1" = "upload_token" ]] && { _upload_token; exit; }
|
||||||
[[ "$1" = "self" ]] && { _self_url; }
|
[[ "$1" = "self" ]] && { _self_url; }
|
||||||
[[ "$1" = "h" || "$1" = "help" ]] && CMD=help
|
[[ "$1" = "h" || "$1" = "help" ]] && CMD=help
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from flask import current_app as app
|
|||||||
import time
|
import time
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import shutil
|
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():
|
def get_db():
|
||||||
@@ -136,6 +136,28 @@ def db_maintenance():
|
|||||||
c.executemany("DELETE FROM files WHERE token = ?", deleted_tokens)
|
c.executemany("DELETE FROM files WHERE token = ?", deleted_tokens)
|
||||||
if c.rowcount > 0:
|
if c.rowcount > 0:
|
||||||
db.commit()
|
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.")
|
messages.append("Maintenance done.")
|
||||||
return "\n".join(messages)
|
return "\n".join(messages)
|
||||||
|
|
||||||
@@ -199,3 +221,41 @@ def file_list():
|
|||||||
|
|
||||||
def file_list_simple():
|
def file_list_simple():
|
||||||
return [f"{r[0]}/{r[1]}" for r in db_get_files()]
|
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()
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import passlib.hash
|
|||||||
VALID_TOKEN_CHARS = string.digits + string.ascii_letters
|
VALID_TOKEN_CHARS = string.digits + string.ascii_letters
|
||||||
|
|
||||||
|
|
||||||
def random_token():
|
def random_token(n=8):
|
||||||
return "".join(secrets.choice(VALID_TOKEN_CHARS) for i in range(8))
|
return "".join(secrets.choice(VALID_TOKEN_CHARS) for i in range(n))
|
||||||
|
|
||||||
|
|
||||||
def file_date_human(num):
|
def file_date_human(num):
|
||||||
|
|||||||
@@ -298,11 +298,26 @@ function t16-download-password() {
|
|||||||
-H "Password: wrongpassword" \
|
-H "Password: wrongpassword" \
|
||||||
"$FLASK_PUBLIC_URL"/d/"$token_name" > downloaded
|
"$FLASK_PUBLIC_URL"/d/"$token_name" > downloaded
|
||||||
file 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() {
|
_getlist() {
|
||||||
declare -F | awk '{ print $3 }' | grep -v ^_
|
declare -F | awk '{ print $3 }' | grep -v ^_
|
||||||
echo exit
|
echo exit
|
||||||
|
|||||||
Reference in New Issue
Block a user