diff --git a/code/app.py b/code/app.py index 051d43b..f544ea8 100644 --- a/code/app.py +++ b/code/app.py @@ -13,7 +13,7 @@ from revprox import ReverseProxied from utils import * -__FLEES_VERSION__ = "20191024.0" +__FLEES_VERSION__ = "20191031.0" app = Flask(__name__) app.config.from_object(__name__) config_values = read_config(app) @@ -30,7 +30,8 @@ if 'notifier' in config_values: app.secret_key = config_values['app_secret_key'] app.wsgi_app = ReverseProxied(app.wsgi_app) - +with app.app_context(): + expire_database_create() @app.before_request def before_request(): @@ -351,6 +352,42 @@ def file_delete(name, token, filename): return "OK", 200 +@app.route('/file/expiring////', methods=['GET']) +def file_expiring(name, token, expire, filename): + (ok,share) = get_share(name, token = token) + if not ok: + return share + full_path = os.path.join( + share['path'], + secure_filename_hidden(filename) + ) + if not os.path.exists(full_path): + return "-1", 403 + allow_direct = get_or_none('direct_links', share) if get_or_none('pass_hash', share) else False + if not allow_direct: + return "-1", 403 + expires = 24 * 3600 * float(expire) + time.time() + return set_expiring_file(share, full_path, expires), 200 + + +@app.route('/file/expiring_remove///', methods=['GET']) +def file_expiring_remove(name, token, filename): + (ok,share) = get_share(name, token = token) + if not ok: + return share + full_path = os.path.join( + share['path'], + secure_filename_hidden(filename) + ) + if not os.path.exists(full_path): + return "-1", 403 + allow_direct = get_or_none('direct_links', share) if get_or_none('pass_hash', share) else False + if not allow_direct: + return "-1", 403 + remove_expiring_file(share, full_path) + return "OK", 200 + + @app.route('/file/direct///', methods=['GET']) def file_direct(name, token, filename): (ok,share) = get_share(name, token = token) @@ -485,6 +522,27 @@ def logout(name): ) +@app.route('/e/', methods=['GET']) +@app.route('/e//', methods=['GET']) +def download_expiring(ehash, filename = None): + file_path, expiring = get_expiring_file(ehash) + if not os.path.exists(file_path): + return 'No such file', 404 + if expiring - time.time() < 0: + return 'Expired', 404 + notify({ + "filename": file_path, + "operation": "expiring_download" + }) + if filename == None: + filename = os.path.basename(file_path) + return send_file( + file_path, + as_attachment = True, + attachment_filename = filename + ) + + @app.route('/direct///', methods=['GET']) def download_direct(name,token,filename): (ok,share) = get_share(name, require_auth = False) @@ -932,9 +990,10 @@ def zip_clean(): os.remove(os.path.join(app.config['ZIP_FOLDER'],file)) +zip_clean() + if __name__ == "__main__": - zip_clean() app.run(debug=True) diff --git a/code/docker-requirements.txt b/code/docker-requirements.txt index 32fa877..19f03a4 100644 --- a/code/docker-requirements.txt +++ b/code/docker-requirements.txt @@ -4,3 +4,4 @@ pycrypto requests python-magic python-dateutil +apsw diff --git a/code/templates/flip b/code/templates/flip index 1792068..a127e8d 100755 --- a/code/templates/flip +++ b/code/templates/flip @@ -15,6 +15,9 @@ _help() { (c)opy Copy file/folder (p)aste Paste file/folder (will overwrite) url Get the direct share url + short Get a short share url. Add argument as days of expiration + Default: 14 days + short-del Delete short share url for a file. upload Get URL for uploads [no arguments] update Update flip client [no arguments] self Get URL to install this client to another computer @@ -194,6 +197,14 @@ _url() { # name curl -L -s "$FLEES_ROOTURL/file/direct/$FLEES_SHARE/$FLEES_TOKEN/$1" echo '' } +_short() { + curl -L -s "$FLEES_ROOTURL/file/expiring/$FLEES_SHARE/$FLEES_TOKEN/$2/$1" + echo '' +} +_short_del() { + curl -L -s "$FLEES_ROOTURL/file/expiring_remove/$FLEES_SHARE/$FLEES_TOKEN/$1" + echo '' +} _upload_url() { echo "This information is a security risk, watch where it's shared" echo "# python2 <( curl -L -s $FLEES_ROOTURL/script/upload_split/$FLEES_SHARE/$FLEES_TOKEN ) file_to_upload.ext" @@ -283,6 +294,8 @@ CMD=list [[ "$1" = "simplelist" ]] && { CMD=simple_list; ARG1=$CMD; } [[ "$1" = "autocomplete" ]] && { _get_completer; } [[ "$1" = "url" ]] && { CMD=url; ARG1=$CMD; } +[[ "$1" = "short" ]] && { CMD=short; ARG1=$CMD; } +[[ "$1" = "short-del" ]] && { CMD=short-del; ARG1=$CMD; } [[ "$1" = "update" ]] && { _update_client; } [[ "$1" = "upload" ]] && { _upload_url; } [[ "$1" = "self" ]] && { _self_url; } @@ -324,3 +337,15 @@ _get_file _url "$NAME" exit $? } +[[ "$CMD" = short ]] && { + EXPIRY=14 + if [ -n "$3" ]; then + EXPIRY="$3" + fi + _short "$NAME" $EXPIRY + exit $? +} +[[ "$CMD" = short-del ]] && { + _short_del "$NAME" + exit $? +} diff --git a/code/utils/crypt.py b/code/utils/crypt.py index 3d86c1c..52dcc50 100644 --- a/code/utils/crypt.py +++ b/code/utils/crypt.py @@ -4,6 +4,7 @@ import base64 #~ from Crypto.Cipher import AES import hashlib + #~ class Crypto: #~ def __init__(self, secret): #~ self.secret = add_pad(secret[0:16]) @@ -68,6 +69,14 @@ def random_token(): return token +def random_expiring_hash(): + codes = [] + for i in range(3): + codes.append("".join([random.choice(string.ascii_letters + string.digits) for n in range(4)])) + ehash = "-".join(codes) + return ehash + + def remove_pad(string): """ Remove spaces from right """ return string.rstrip(" ") diff --git a/code/utils/files.py b/code/utils/files.py index b20f4bf..6c8af9c 100644 --- a/code/utils/files.py +++ b/code/utils/files.py @@ -5,6 +5,11 @@ import requests import re import json import stat +import time +try: + import apsw +except ImportError as e: + pass from .misc import * from .crypt import * @@ -76,6 +81,26 @@ def download_url(url, filename): return (True, ("OK", 200 )) +def expire_database_create(): + connection = apsw.Connection(app.config['SQLITE_FILE']) + cursor = connection.cursor() + try: + cursor.execute("""CREATE TABLE IF NOT EXISTS expiring ( + hash text PRIMARY KEY, + file text NOT NULL, + expires integer NOT NULL + );""") + cursor.execute("DELETE FROM expiring WHERE expires < ?", + ( + time.time(), + ) + ) + except apsw.BusyError as e: + # Other thread is creating the database + pass + set_rights(app.config['SQLITE_FILE']) + + def file_autoremove(path, share, notifier = None): autoremove = get_or_none('autoremove', share, 0) if autoremove == 0: @@ -207,6 +232,17 @@ def get_download_url(share, file, token): )) +def get_expiring_file(ehash): + connection = apsw.Connection(app.config['SQLITE_FILE']) + cursor = connection.cursor() + + for row in cursor.execute("SELECT file, expires FROM expiring WHERE hash = ?", + ( ehash, ) + ): + return row[0], row[1] + return None, None + + def get_script_url(public_url, share, end_point, token = "[TOKEN]"): cmd = None doc = None @@ -267,6 +303,7 @@ def read_config(app): app.config['SITE_NAME'] = config_values['site_name'] app.config['UPLOAD_FOLDER'] = config_values['data_folder'] app.config['SHARES_FILE'] = config_values['shares_file'] + app.config['SQLITE_FILE'] = config_values['sqlite_file'] if 'log_file' in config_values: app.config['LOG_FILE'] = config_values['log_file'] else: @@ -294,3 +331,41 @@ def set_rights(path): os.chmod(path, st.st_mode | stat.S_IRUSR | stat.S_IWUSR) if app.config['GID'] > 0: os.chmod(path, st.st_mode | stat.S_IRGRP | stat.S_IWGRP) + + +def set_expiring_file(share, filename, expires): + connection = apsw.Connection(app.config['SQLITE_FILE']) + cursor = connection.cursor() + + while True: + ehash = random_expiring_hash() + matches = len(list(cursor.execute("SELECT file FROM expiring WHERE hash = ?", + ( ehash, ) + ))) + if matches == 0: + break + + cursor.execute("INSERT INTO expiring (hash, file, expires) VALUES (?,?,?)", + ( + ehash, + filename, + expires + ) + ) + return "/".join(( + app.config['PUBLIC_URL'], + 'e', + ehash, + os.path.basename(filename) + )) + + +def remove_expiring_file(share, filename): + connection = apsw.Connection(app.config['SQLITE_FILE']) + cursor = connection.cursor() + + cursor.execute("DELETE FROM expiring WHERE file = ?", + ( + filename, + ) + ) diff --git a/data/config.json.example b/data/config.json.example index b9daed7..84c3619 100644 --- a/data/config.json.example +++ b/data/config.json.example @@ -19,6 +19,7 @@ "notifier": "", "__do_not_edit": "most likely you will not change anything after this line", "data_folder": "data", + "sqlite_file": "data/expiring.sqlite", "shares_file": "data/shares.json", "log_file": "data/flees.log", "version_folder": "_version",