# -*- coding: utf-8 -*- import os import sys import time from flask import ( Flask, 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, hash_password, verify_password, file_date_human from utils.files import ( db_store_file, file_details, file_list, file_list_simple, file_full_path, file_full_url, db_add_download, db_get_file, db_delete_file, db_maintenance, validate_upload_token, invalidate_upload_token, new_upload_token, ) import logging logging.basicConfig( level=logging.INFO, format=f"[%(asctime)s] [%(levelname)s] %(message)s", ) __VERSION__ = "20230828.0" app = Flask(__name__) app.config.from_object(__name__) app.config.from_prefixed_env() app.debug = True app.secret_key = app.config["APP_SECRET_KEY"] app.wsgi_app = ReverseProxied(app.wsgi_app) @app.before_request def log_request_info(): pass @app.after_request def log_the_status_code(response): ip = request.environ.get("HTTP_X_FORWARDED_FOR", request.remote_addr) app.logger.info(f"{request.method}:{response.status_code} {request.url} {ip}") return response @app.route("/") def index(): """Returns Nothing""" return "", 200 @app.route('/health.html', methods=["GET",]) # fmt: skip def health(): return f"OK {request.url}", 200 @app.route("/upload", methods=["PUT", "POST"]) def upload(): """ Upload a file, example CURL: cat file | \ curl -fL -w "\n" -F file="@-" -X POST \ -H "Name: my.file.ext" \ -H "Max-Downloads: 4000" \ -H "Expires-Days: 14" \ -H "Password: mypass" \ -H "Secret: dff789f0bbe8183d32542" \ "$FLASK_PUBLIC_URL"/upload - Additionally, "Expires-Hours" can be used. - Max-Dowloads: -1 means no upper limit IF using GET, you can upload larger files with pipes cat largefile | \ curl -fL -w "\n" --upload-file - \ -H "Name: my.file.ext" \ -H "Max-Downloads: 4000" \ -H "Expires-Days: 14" \ -H "Secret: dff789f0bbe8183d32542" \ "$FLASK_PUBLIC_URL"/upload Returns the file download URL """ name = request.headers.get("Name", None) if name is None: return "Name required", 500 safe_filename = secure_filename(name) 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"]: 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: expires = int(time.time()) + 24 * 3600 * int( request.headers.get("Expires-days") ) 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) if request.method == "POST": file = request.files.get("file") if file: file.save(filename) else: return "Use the 'file' variable to upload\n", 400 if request.method == "PUT": chunk_size = 1024 * 1024 * 64 # 64Mb with open(filename, "wb") as f: while True: chunk = request.stream.read(chunk_size) if not chunk: break f.write(chunk) db_store_file(token, safe_filename, expires, max_dl, password) download_url = file_full_url(token, safe_filename) 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): """ Get JSON of file details. Size, added date, download times, etc. curl -fL -w "\n" \ -H "Secret: dff789f0bbe8183d3254258b33a147d580c1131f39a698c56d3f640ac8415714" \ "$ROOTURL"/details/OdD7X0aKOGM/big_file1.ext """ secret = request.headers.get("Secret", "") if secret != app.config["ACCESS_TOKEN"]: return "Error", 401 details = file_details(token, name) return jsonify(details), 200 @app.route("/delete//", methods=["GET"]) def delete_file(name, token): """ Delete a file from the system """ secret = request.headers.get("Secret", "") if secret != app.config["ACCESS_TOKEN"]: return "Error", 401 try: os.remove(os.path.join(app.config["DATAFOLDER"], token, name)) except Exception: pass db_delete_file(token, name) return "OK", 200 @app.route("/ls", methods=["GET"]) def ls(): """ Lists all uploaded files """ secret = request.headers.get("Secret", "") if secret != app.config["ACCESS_TOKEN"]: return "Error", 401 return "\n".join(file_list()), 200 @app.route("/ls-simple", methods=["GET"]) def ls_simple(): """ Lists token/names """ secret = request.headers.get("Secret", "") if secret != app.config["ACCESS_TOKEN"]: return "Error", 401 return "\n".join(file_list_simple()), 200 @app.route("/maintenance", methods=["GET"]) def maintenance(): """ Clears DB of expired entries. Deletes folders without DB entry """ secret = request.headers.get("Secret", "") if secret != app.config["ACCESS_TOKEN"]: return "Error", 401 return db_maintenance(), 200 @app.route("/d//", methods=["GET"]) def download(name, token): """ Download a file """ if "Password" in request.headers: session[token] = request.headers["Password"] return download_file(token, name) @app.route("/script/mfl", methods=["GET"]) def script_mfl(): secret = request.headers.get("Secret", "") if secret != app.config["ACCESS_TOKEN"]: return "Error", 401 return render_template( "mfl", token=app.config["ACCESS_TOKEN"], rooturl=app.config["PUBLIC_URL"], version=__VERSION__, ) @app.route("/login", methods=["GET", "POST"]) def login(): if request.method == "POST": session[request.form["token"]] = request.form["password"] return redirect(request.form["redirect"]) if set(("name", "redirect", "token")).issubset(session.keys()): return render_template( "login.html", filename=session["name"], redirect=session["redirect"], token=session["token"], ) return "", 400 @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 """ full_path = file_full_path(token, name) if not os.path.exists(full_path): return "Error", 404 db_stat = db_get_file(token, name) if 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 ) def print_debug(s): if app.config["DEBUG"]: sys.stderr.write(str(s) + "\n") sys.stderr.flush() if __name__ == "__main__": app.run(debug=True)