fileHost/fileHost.py

481 lines
12 KiB
Python
Raw Permalink Normal View History

2018-02-19 23:01:28 -05:00
#!/usr/bin/env python3
"""
Simple file host using Flask.
"""
import os
2019-04-02 10:12:58 -04:00
import re
2018-05-21 14:27:51 -04:00
import time
import atexit
2018-02-19 23:01:28 -05:00
import string
2018-04-14 20:34:49 -04:00
import secrets
import sqlite3
2019-04-02 10:12:58 -04:00
import tempfile
import functools
2018-04-14 20:34:49 -04:00
import threading
2018-05-17 12:46:36 -04:00
from datetime import datetime
2018-02-19 23:01:28 -05:00
from passlib.hash import argon2
from flask import Flask, session, request, abort, redirect, url_for, g, \
render_template
from werkzeug.utils import secure_filename
2019-04-02 10:12:58 -04:00
from werkzeug.datastructures import FileStorage
from flask_paranoid import Paranoid
2018-06-12 08:51:54 -04:00
from apscheduler.schedulers.background import BackgroundScheduler
2019-04-02 10:12:58 -04:00
import requests
2018-02-19 23:01:28 -05:00
2019-09-29 14:49:06 -04:00
import buckler_flask
2018-02-19 23:01:28 -05:00
app = Flask(__name__)
2019-09-29 14:49:06 -04:00
app.session_interface = buckler_flask.BucklerSessionInterface()
app.before_request(buckler_flask.require_auth)
2018-02-19 23:01:28 -05:00
app.config['MAX_CONTENT_LENGTH'] = 128 * 1024 * 1024
2019-04-02 10:12:58 -04:00
app.config["UPLOAD_DIR"] = "/var/www/html/up"
2018-04-14 21:27:43 -04:00
app.config["UPLOAD_URL"] = "https://steelbea.me/up/"
2018-02-19 23:01:28 -05:00
app.config["DB_NAME"] = "fileHost.db"
2018-04-14 20:34:49 -04:00
app.config["DB_LOCK"] = threading.Lock()
paranoid = Paranoid(app)
paranoid.redirect_view = 'login'
2018-06-12 08:51:54 -04:00
scheduler = BackgroundScheduler(timezone="America/New_York")
scheduler.start()
2018-04-14 20:34:49 -04:00
def db_execute(*args, **kwargs):
"""
Opens a connection to the app's database and executes the SQL statements
passed to this function.
"""
with sqlite3.connect(app.config.get("DB_NAME")) as con:
app.config.get("DB_LOCK").acquire()
cur = con.cursor()
res = cur.execute(*args, **kwargs)
app.config.get("DB_LOCK").release()
return res
2018-02-19 23:01:28 -05:00
2018-06-12 08:51:54 -04:00
@scheduler.scheduled_job("interval", minutes=1)
def delete_this():
"""
Removes files that are past the expiration date.
"""
records = db_execute(
"SELECT filename, delete_date FROM uploads WHERE delete_date"
).fetchall()
for filename, delete_date in records:
if time.time() >= delete_date:
delete_file(filename)
2018-02-19 23:01:28 -05:00
def init():
"""
Initializes the application.
"""
2018-04-06 19:18:45 -04:00
os.makedirs(app.config.get("UPLOAD_DIR"), exist_ok=True)
2018-02-19 23:01:28 -05:00
# init secret key
2018-04-06 19:18:45 -04:00
if os.path.exists("secret_key"):
with open("secret_key", "rb") as file:
secret_key = file.read()
else:
secret_key = os.urandom(64)
with open("secret_key", "wb") as file:
file.write(secret_key)
app.secret_key = secret_key
2018-02-19 23:01:28 -05:00
# init db
2018-04-06 19:18:45 -04:00
try:
2018-04-14 20:34:49 -04:00
db_execute("SELECT * FROM users").fetchone()
db_execute("SELECT * FROM uploads").fetchone()
2018-04-06 19:18:45 -04:00
except sqlite3.OperationalError:
2018-04-14 20:34:49 -04:00
db_execute("CREATE TABLE users("
2018-04-06 19:18:45 -04:00
"id INTEGER PRIMARY KEY,"
"username TEXT,"
"pw_hash TEXT,"
"admin BOOL DEFAULT FALSE)")
2018-04-14 20:34:49 -04:00
db_execute("CREATE TABLE uploads("
2018-05-21 14:27:51 -04:00
"filename TEXT,"
"uploaded_by TEXT,"
"uploaded_date INTEGER DEFAULT (STRFTIME('%s', 'now')),"
"delete_date INTEGER)")
2018-02-19 23:01:28 -05:00
def add_user(username, password, admin="FALSE"):
"""
Adds a user to the database.
"""
2018-04-14 20:34:49 -04:00
u = db_execute("SELECT username FROM users WHERE username = ?",
2018-02-19 23:01:28 -05:00
(username,)).fetchone()
if u:
return False
pw_hash = argon2.hash(password)
2018-04-14 20:34:49 -04:00
db_execute("INSERT INTO users (username, pw_hash, admin) VALUES (?,?,?)",
2018-02-19 23:01:28 -05:00
(username, pw_hash, admin))
return True
def verify_password(username, password):
"""
Verifies a user's password.
"""
user = verify_username(username)
if not user:
return False
_, _, pw_hash, admin = user
if argon2.verify(password, pw_hash):
g.user = username
g.admin = admin == "TRUE"
return True
else:
return False
def verify_username(username):
"""
Checks to see if the given username is in the database.
"""
2018-04-14 20:34:49 -04:00
user = db_execute("SELECT * FROM users WHERE username = ?",
2018-02-19 23:01:28 -05:00
(username,)).fetchone()
if user:
return user
else:
return False
2018-05-17 12:46:36 -04:00
def delete_file(filename):
"""
Deletes a file from the upload directory and from the database.
"""
try:
os.remove(os.path.join(app.config.get("UPLOAD_DIR"), filename))
db_execute("DELETE FROM uploads WHERE filename = ?", (filename,))
except FileNotFoundError:
return False
return True
def login_required(url=None):
"""
A decorator function to protect certain endpoints by requiring the user
2019-04-02 10:12:58 -04:00
to either pass a valid session cookie, or pass their username and
password along with the request to login.
"""
def actual_decorator(func):
@functools.wraps(func)
def _nop(*args, **kwargs):
2019-09-29 14:49:06 -04:00
username = session.meta['username']
if verify_username(username):
return func(*args, **kwargs)
username = request.form.get("user")
password = request.form.get("pass")
if verify_password(username, password):
session["username"] = username
return func(*args, **kwargs)
if url:
return redirect(url_for(url))
else:
abort(401)
return _nop
return actual_decorator
2018-05-17 12:46:36 -04:00
2018-02-19 23:01:28 -05:00
@app.route("/delete_file", methods=["POST"])
2018-05-17 12:46:36 -04:00
def deleteFile():
2018-02-19 23:01:28 -05:00
"""
2018-05-17 12:46:36 -04:00
Allows a user to delete a file from the upload directory and the database.
2018-02-19 23:01:28 -05:00
"""
2019-09-29 14:49:06 -04:00
username = session.meta['username']
2018-02-19 23:01:28 -05:00
filename = request.form.get("fname")
2018-05-17 12:46:36 -04:00
if not verify_username(username):
2018-02-19 23:01:28 -05:00
abort(401)
if not g.admin:
2018-05-17 12:46:36 -04:00
uploader = db_execute(
"SELECT uploaded_by FROM uploads WHERE filename=?",
(filename,)).fetchone()[0]
if uploader != username:
abort(401)
2018-02-19 23:01:28 -05:00
2018-05-17 12:46:36 -04:00
res = delete_file(filename)
if res:
return "Success"
else:
2018-02-19 23:01:28 -05:00
return "Error: File not found."
@app.route("/add_user", methods=["POST"])
def addUser():
"""
Allows an admin to add a user via API POST. No frontend allowed.
"""
username = request.form.get("user")
password = request.form.get("pass")
new_username = request.form.get("new_user")
new_password = request.form.get("new_pass")
admin = request.form.get("admin") or "FALSE"
if not verify_password(username, password):
abort(401)
if not g.admin:
abort(401)
res = add_user(new_username, new_password, admin)
if res:
return "Success"
else:
return "Username already exists."
@app.route("/logout", methods=["GET"])
2018-02-19 23:01:28 -05:00
def logout():
"""
Logs the user out and removes his session cookie.
"""
session.pop("username")
return redirect(url_for("login"))
@app.route("/change_password", methods=["POST", "GET"])
2019-09-29 14:49:06 -04:00
#@login_required()
2018-02-19 23:01:28 -05:00
def change_password():
"""
Allows the user to change their password.
"""
if request.method == "GET":
return render_template("change_password.html")
2019-09-29 14:49:06 -04:00
username = session.meta['username']
2018-02-19 23:01:28 -05:00
current_password = request.form.get("current_password")
new_password = request.form.get("new_password")
new_password_verify = request.form.get("new_password_verify")
if not verify_password(username, current_password):
return "The current password does not match!"
if new_password != new_password_verify:
return "The new passwords do not match!"
pw_hash = argon2.hash(new_password)
2018-04-14 20:34:49 -04:00
db_execute("UPDATE users SET pw_hash = ? WHERE username = ?",
2018-02-19 23:01:28 -05:00
(pw_hash, username))
session.pop("username")
return redirect(url_for("login"))
@app.route("/login", methods=["POST", "GET"])
def login():
"""
Logs the user in.
"""
if request.method == "GET":
return render_template("login.html")
username = request.form.get("user")
password = request.form.get("pass")
if verify_password(username, password):
session["username"] = username
return redirect(url_for("index"))
else:
abort(401)
2018-04-14 21:27:43 -04:00
@app.route("/manage_uploads", methods=["POST", "GET"])
2019-09-29 14:49:06 -04:00
#@login_required()
2018-04-14 21:27:43 -04:00
def manage_uploads():
"""
Allows the user to view and/or delete uploads they've made.
"""
2019-09-29 14:49:06 -04:00
username = session.meta['username']
2018-04-14 21:27:43 -04:00
if request.method == "GET":
2018-05-17 12:46:36 -04:00
uploads = db_execute(
"SELECT filename, uploaded_date FROM uploads WHERE uploaded_by = ?",
(username,)).fetchall()
new_uploads = []
for file, date in uploads:
date = datetime.fromtimestamp(date).strftime("%Y-%m-%d %H:%M")
new_uploads.append((file, date))
return render_template("manage_uploads.html", uploads=new_uploads,
2018-05-21 14:27:51 -04:00
upload_url=app.config.get("UPLOAD_URL"))
2018-05-17 12:46:36 -04:00
deletes = [fname for fname,_ in request.form.items()]
deletes.remove("submit")
for filename in deletes:
uploader = db_execute(
"SELECT uploaded_by FROM uploads WHERE filename=?",
(filename,)).fetchone()[0]
if uploader != username:
abort(401)
delete_file(filename)
return redirect(url_for("manage_uploads"))
2018-04-14 21:27:43 -04:00
2018-05-22 09:42:02 -04:00
@app.route("/gallery/<path:username>", methods=["GET"])
def gallery(username):
"""
Displays a publicly accessable gallery of files the user has uploaded.
"""
if not verify_username(username):
return "User not found, or user has gallery disabled."
uploads = db_execute(
"SELECT filename, uploaded_date FROM uploads WHERE uploaded_by = ?",
(username,)).fetchall()
new_uploads = []
for file, date in uploads:
date = datetime.fromtimestamp(date).strftime("%Y-%m-%d %H:%M")
new_uploads.append((file, date))
return render_template("gallery.html", uploads=new_uploads, user=username,
upload_url=app.config.get("UPLOAD_URL"))
@app.route("/upload", methods=["POST"])
2019-09-29 14:49:06 -04:00
#@login_required("login")
def upload():
2018-02-19 23:01:28 -05:00
"""
Saves the uploaded files and returns URLs pointing to them.
2018-02-19 23:01:28 -05:00
"""
2019-09-29 14:49:06 -04:00
username = session.meta['username']
2019-04-02 10:12:58 -04:00
if request.form.get("url"):
try:
f = [download_file(request.form.get("url"))]
except ValueError as e:
2019-04-12 18:53:16 -04:00
return str(e)
if request.form.get("text"):
try:
f = [text_paste(request.form.get("text"))]
except Exception as e:
return str(e)
2019-04-02 10:12:58 -04:00
else:
f = []
2018-06-05 06:58:18 -04:00
urls = []
2019-04-02 10:12:58 -04:00
for file in request.files.getlist("files") + f:
2018-06-05 06:58:18 -04:00
fname = secure_filename(file.filename)
pre = get_rand_chars(8)
fdir = app.config.get("UPLOAD_DIR")
2019-04-12 18:53:16 -04:00
if request.form.get("randname") == "on" or request.form.get("text"):
2018-06-05 06:58:18 -04:00
fname = pre + os.path.splitext(fname)[1]
2019-04-12 18:53:16 -04:00
if request.form.get("text"):
fname += ".txt"
2018-06-05 06:58:18 -04:00
else:
fname = pre + "_" + fname
if request.form.get("delflag") == "on":
try:
delete_time = int(request.form.get("delnum"))
assert delete_time >= 1 and delete_time <= 59
except (ValueError, AssertionError):
return 'Invalid entry: "delnum=' + request.form.get("delnum") + '"'
del_dict = {"minute": 60, "hour": 3600, "day": 3600*24, "week": 3600*24*7}
try:
delete_time *= del_dict[request.form.get("deltype")]
except KeyError:
return 'Invalid entry: "deltype=' + request.form.get("deltype")+'"'
delete_time = int(time.time()) + delete_time
db_execute(
"INSERT INTO UPLOADS (filename, uploaded_by, delete_date)"
"VALUES (?,?,?)", (fname, username, delete_time))
else:
db_execute("INSERT INTO uploads (filename, uploaded_by) VALUES (?,?)",
(fname, username))
file.save(os.path.join(fdir, fname))
url = app.config.get("UPLOAD_URL") + fname
urls.append(url)
if request.form.get("html"):
return render_template("result.html", urls=urls)
else:
return "\n".join(urls)
@app.route("/", methods=["GET", "POST"])
2019-09-29 14:49:06 -04:00
#@login_required("login")
def index():
"""
Saves the uploaded file and returns a URL pointing to it.
"""
if request.method == "GET":
return render_template("index.html")
else:
# so I can point my cli script to /upload/ instead of /upload/upload/
return upload()
2018-02-19 23:01:28 -05:00
def get_rand_chars(n):
"""
Returns n number of random characters. Character set includes lowercase
and uppercase ascii letters and digits.
"""
chars = []
for _ in range(n):
char = secrets.choice(string.ascii_letters + string.digits)
chars.append(char)
return "".join(chars)
2019-04-02 10:12:58 -04:00
def download_file(url):
"""
Downloads the file at the given url while observing file size and
timeout limitations.
"""
timeout = 10
requests_kwargs = {
'stream': True,
'headers': {'User-Agent': "Steelbea.me LTD needs YOUR files."},
'timeout': timeout,
'verify': True
}
t = tempfile.TemporaryFile()
with requests.get(url, **requests_kwargs) as r:
size = 0
start_time = time.time()
for chunk in r.iter_content(102400):
if time.time() - start_time > timeout:
raise ValueError('timeout reached')
size += len(chunk)
if size > app.config['MAX_CONTENT_LENGTH']:
raise ValueError('response too large')
t.write(chunk)
if r.headers.get('Content-Disposition'):
fname = re.search(r'filename="(.+)"',
r.headers['content-disposition'])
else:
fname = os.path.basename(url)
t.seek(0)
f = FileStorage(stream=t, filename=fname, name='url')
return f
2019-04-12 18:53:16 -04:00
def text_paste(text):
"""
Simple wrapper to handle a text paste upload.
"""
t = tempfile.TemporaryFile()
t.write(bytes(text, encoding='utf8'))
t.seek(0)
f = FileStorage(stream=t, filename="", name='text')
return f
2018-04-14 20:34:49 -04:00
init()
2018-06-12 08:51:54 -04:00
atexit.register(scheduler.shutdown)
2018-02-19 23:01:28 -05:00
if __name__ == "__main__":
2018-04-06 19:18:45 -04:00
import sys
2018-04-14 21:27:43 -04:00
if len(sys.argv) > 1:
add_user(sys.argv[1], sys.argv[2], "TRUE")
2018-02-19 23:01:28 -05:00
app.run(host='0.0.0.0', port=5000)