#!/usr/bin/python # -*- coding: utf-8 -*- import os,sys,time,stat import json from datetime import datetime from flask import Flask, render_template, jsonify, current_app, \ redirect, url_for, request, g, session, send_file, send_from_directory from werkzeug.utils import secure_filename import hashlib import zipfile from revprox import ReverseProxied __FLEES_VERSION__ = "20180219.0" app = Flask(__name__) app.config.from_object(__name__) # Read config from json ! config_values = json.load(open(os.getenv('FLEES_CONFIG'),'rt')) 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['ZIP_FOLDER'] = config_values['zip_folder'] app.config['MAX_ZIP_SIZE'] = config_values['max_zip_size'] # megabytes app.config['DATE_FORMAT'] = config_values['date_format'] app.config['UID'] = config_values['uid'] app.config['GID'] = config_values['gid'] app.config['DEBUG'] = config_values['debug'] if 'notifier' in config_values: if len(config_values['notifier']) > 0: notifier_config = config_values['notifier'].split(":") imported = getattr(__import__( notifier_config[0], fromlist=[notifier_config[1]] ), notifier_config[1] ) app.config['notifier'] = imported() app.secret_key = config_values['app_secret_key'] app.wsgi_app = ReverseProxied(app.wsgi_app) @app.before_request def before_request(): g.shares = json.load(open(app.config['SHARES_FILE'],'rt')) g.version = __FLEES_VERSION__ g.site_name = app.config['SITE_NAME'] g.max_zip_size = app.config['MAX_ZIP_SIZE'] @app.route("/") def index(): public_shares = [] for share in g.shares: public = get_or_none(share,'public') expired = is_expired(share) authenticated_share = get_share(share['name']) password_set = False if authenticated_share[0]: password_set = authenticated_share[1]['authenticated'] == 'hash' if not expired: if public or password_set: public_shares.append({ 'name': share['name'], 'expire': get_or_none(share,'expire'), 'upload': get_or_none(share,'upload'), 'password_set': password_set, 'description': get_or_none(share,'description','') }) return render_template("index.html", entries=public_shares) @app.route('/authenticate/', methods=['GET','POST']) def authenticate(name): if request.method == 'GET': return render_template('authenticate.html',name=name) if request.method == 'POST': user_password = request.form['password'].encode('utf-8') session[name] = hashlib.sha1(user_password).hexdigest() return redirect(url_for('list_view',name=name)) @app.route('/upload//', methods=['POST']) @app.route('/upload', methods=['POST']) def upload(name = None, password = None): if request.method == 'POST': file = request.files['file'] if name == None: name = request.form['name'] if password != None: session[name] = password (ok,share) = get_share(name) if not ok: return share if not get_or_none(share,'upload') == True: return "Upload not allowed",400 if file: filename = os.path.join( share['path'], secure_filename( file.filename ) ) if get_or_none(share, 'overwrite') == False: if os.path.exists(filename): return "Overwrite forbidden", 403 file.save(filename) set_rights(filename) notify({ "recipient": get_or_none(share,'recipient'), "share": name, "filename": filename, "operation": "upload", "address": request.remote_addr }) if 'from_gui' in request.form: if request.form['from_gui'] == "true": return redirect(url_for('list_view',name=name)) return "File uploaded\n", 200 else: return "Use the 'file' variable to upload",400 @app.route('/send/', methods=['GET']) def send(name): (ok,share) = get_share(name) if not ok: return share return render_template('send.html',name=name) @app.route('/list//', methods=['GET']) @app.route('/list/', methods=['GET']) def list_view(name, password = None): if password != None: session[name] = password (ok,share) = get_share(name) if not ok: return share files = [] for file in sorted(os.listdir(share['path'])): fp = os.path.join(share['path'],file) if os.path.isdir(fp): continue if file.startswith("."): continue status = file_stat(fp) status.update({ 'token': get_direct_token(share, file) }) files.append(status) # direct share links not allowed if password isnt set allow_direct = get_or_none(share,'direct_links') if get_or_none(share,'pass_hash') else False return render_template( "list.html", name = share['name'], entries = files, password = get_or_none(share,'pass_hash'), public = get_or_none(share,'public'), upload = get_or_none(share,'upload'), overwrite = get_or_none(share,'overwrite'), direct = allow_direct, expire = get_or_none(share,'expire'), description = get_or_none(share,'description',"") ) @app.route('/logout/', methods=['GET']) def logout(name, password = None): if name in session: del session[name] return render_template( "logout.html", name = name ) @app.route('/direct///', methods=['GET']) def download_direct(name,password,filename): if password != None: session[name] = password (ok,share) = get_share(name, require_auth = False) if not ok: return share allow_direct = get_or_none(share,'direct_links') if allow_direct != True: return 'Direct download not allowed', 403 token = get_direct_token(share, filename) if token == None: return 'Cannot generate token', 400 if password != token: return 'Incorrect token', 403 file_path = os.path.join(share['path'], filename) if not os.path.exists(file_path): return 'no such file', 404 notify({ "recipient": get_or_none(share,'recipient'), "share": name, "filename": file_path, "operation": "direct_download", "address": request.remote_addr }) return send_from_directory(directory=share['path'], filename=filename) @app.route('/download///', methods=['GET']) @app.route('/download//', methods=['GET']) def download_file(name,filename,password = None): if password != None: session[name] = password (ok,share) = get_share(name) if not ok: return share file_path = os.path.join(share['path'], filename) if not os.path.exists(file_path): return 'no such file', 404 notify({ "recipient": get_or_none(share,'recipient'), "share": name, "filename": file_path, "operation": "download", "address": request.remote_addr }) return send_from_directory(directory=share['path'], filename=filename) @app.route('/zip//', methods=['GET']) @app.route('/zip/', methods=['GET']) def download_zip(name,password = None): if password != None: session[name] = password (ok,share) = get_share(name) if not ok: return share folder_size = get_folder_size(share['path']) if folder_size/(1024*1024) > app.config['MAX_ZIP_SIZE']: return "Maximum ZIP size exceeded", 400 zip_clean() zip_path = zip_share(share) notify({ "recipient": get_or_none(share,'recipient'), "share": name, "filename": name + ".zip", "operation": "zip_download", "address": request.remote_addr }) return send_file( zip_path, as_attachment = True, attachment_filename = name + ".zip" ) @app.route('/script/upload//', methods=['GET']) def script_upload(name = None, password = None): session[name] = password (ok,share) = get_share(name) if not ok: return share if not get_or_none(share,'upload') == True: return "Upload not allowed",400 return """#!/bin/bash test -f "$1" || { echo "Add files to upload as argument" exit 1 } CAT=$( which cat ) which pv &> /dev/null && CAT=$( which pv ) for file_name in "$@"; do base_name=$( basename "$file_name" ) test -f "$file_name" || { echo "'$file_name' not a file" continue } $CAT "$file_name" | curl -F "file=@-;filename=${base_name}" %supload/%s/%s done """%( request.url_root, name, password ) def file_stat(filename): s = os.stat(filename) return { 'size': file_size_MB(s.st_size), 'mtime': file_date_human(s.st_mtime), 'name': os.path.basename(filename) } def file_size_human(num): for x in [' B','KB','MB','GB','TB']: if num < 1024.0: if x==' B': return "%d %s" % (num, x) return "%3.1f %s" % (num, x) num /= 1024.0 def file_size_MB(num): return "{:,.2f}".format(num/(1024*1024)) def file_date_human(num): return datetime.fromtimestamp( num ).strftime(app.config['DATE_FORMAT']) def get_direct_token(share, filename): if not 'pass_hash' in share: return None return hashlib.sha1( share['pass_hash'].encode('utf-8') + filename.encode('utf-8') ).hexdigest() def get_folder_size(path): total_size = 0 for filename in os.listdir(path): fp = os.path.join(path, filename) if os.path.isdir(fp): continue total_size += os.path.getsize( fp ) return total_size def get_or_none(d,key,none = None): if key in d: return d[key] else: return none def get_share(name, require_auth = True): share = [x for x in g.shares if x['name'] == name] if len(share) < 1: return (False,redirect(url_for('authenticate',name=name))) share = share[0] if is_expired(share): return (False, 'Share has expired') authenticated = "no-pass" if require_auth: if 'pass_hash' in share: authenticated = False if name in session: if session[name] == share['pass_hash']: authenticated = "hash" if not authenticated: return (False,redirect(url_for('authenticate',name=name))) if not 'path' in share: return (False,'no path defined') share.update({ "path": os.path.join( app.config['UPLOAD_FOLDER'], share['path'] ), "authenticated": authenticated }) if not os.path.exists(share['path']): makedirs_rights(share['path']) return (True,share) def is_expired(share): expires = get_or_none(share, 'expire') if expires: if datetime.now() > datetime.strptime(expires, app.config['DATE_FORMAT']): return True return False def print_debug(s): if app.config['DEBUG']: sys.stderr.write(str(s)+"\n") sys.stderr.flush() def makedirs_rights(path): # os.makedirs with chown path_list = path.split(os.sep) for p in range(len(path_list)): current_path = os.sep.join(path_list[0:(p+1)]) if not os.path.exists(current_path): os.mkdir(current_path) set_rights(current_path) def notify(msg): if 'notifier' in app.config: app.config['notifier'].notify(msg) def set_rights(path): os.chown(path, app.config['UID'], app.config['GID']) st = os.stat(path) if app.config['UID'] > 0: 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 zip_share(share): if not os.path.exists(app.config['ZIP_FOLDER']): os.makedirs(app.config['ZIP_FOLDER']) set_rights(app.config['ZIP_FOLDER']) zip_path = os.path.join( app.config['ZIP_FOLDER'], "%s-%d.zip"%( share['name'], time.time() ) ) zf = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) for file in sorted(os.listdir(share['path'])): fp = os.path.join(share['path'], file) if os.path.isdir(fp): continue print_debug(fp) zf.write( fp, arcname = os.path.join(share['name'],file) ) zf.close() set_rights(zip_path) return zip_path def zip_clean(): """ delete zip files older than 1 hour """ if not os.path.exists(app.config['ZIP_FOLDER']): return for file in os.listdir(app.config['ZIP_FOLDER']): if not file.endswith("zip"): continue mtime = os.stat( os.path.join(app.config['ZIP_FOLDER'],file) ).st_mtime if mtime + 3600 < time.time(): os.remove(os.path.join(app.config['ZIP_FOLDER'],file)) if __name__ == "__main__": app.run(debug=True)