From 00008c9c7d11d3d40e801ee4806c2324d1ef47c7 Mon Sep 17 00:00:00 2001 From: Ville Rantanen Date: Sat, 10 Mar 2018 15:28:34 +0200 Subject: [PATCH] new version allows downloads from subfolders --- code/app.py | 91 ++++++++++++++++++++++------------------ code/flees-manager.py | 28 ++++++------- code/templates/list.html | 2 +- code/utils/utils.py | 43 ++++++++++++++++++- 4 files changed, 108 insertions(+), 56 deletions(-) diff --git a/code/app.py b/code/app.py index 4689bea..60aea46 100644 --- a/code/app.py +++ b/code/app.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- import os,sys,time,stat @@ -8,13 +7,13 @@ from flask import Flask, render_template, jsonify, current_app, Response, \ redirect, url_for, request, g, session, send_file, send_from_directory from werkzeug.utils import secure_filename import zipfile -import urllib from multiprocessing import Process from revprox import ReverseProxied from utils.utils import * from utils.crypt import * -__FLEES_VERSION__ = "20180302.0" + +__FLEES_VERSION__ = "20180310.0" app = Flask(__name__) app.config.from_object(__name__) # Read config from json ! @@ -42,6 +41,7 @@ if 'notifier' in config_values: 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')) @@ -49,6 +49,7 @@ def before_request(): g.site_name = app.config['SITE_NAME'] g.max_zip_size = app.config['MAX_ZIP_SIZE'] + @app.route("/") def index(): public_shares = [] @@ -71,6 +72,7 @@ def index(): return render_template("index.html", entries=public_shares) + @app.route('/authenticate/', methods=['GET','POST']) def authenticate(name): if request.method == 'GET': @@ -80,6 +82,7 @@ def authenticate(name): session[name] = password_hash(user_password, app.secret_key) return redirect(url_for('list_view',name=name)) + @app.route('/upload//', methods=['POST']) @app.route('/upload', methods=['POST']) def upload(name = None, token = None): @@ -158,9 +161,6 @@ def upload_join_splitted(name, token): except: return "Joining failed\n", 400 return "Joining started\n", 200 - #~ return Response(joiner(target_name, parts), mimetype="text/plain", content_type="text/event-stream") - - #~ return "%d parts joined"%(len(parts),), 200 @app.route('/send/', methods=['GET']) @@ -170,22 +170,19 @@ def send(name): return share return render_template('send.html',name=name) + @app.route('/files//', methods=['GET']) def list_files(name, token): (ok,share) = get_share(name, token = token) 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 - files.append(urllib.parse.quote_plus(file)) + for file in iter_folder_files(share['path']): + files.append(path2url(file)) files.append("") return "\n".join(files), 200 + @app.route('/list//', methods=['GET']) @app.route('/list/', methods=['GET']) def list_view(name, token = None): @@ -197,15 +194,13 @@ def list_view(name, token = None): return redirect(url_for('list_view',name=name)) files = [] - for file in sorted(os.listdir(share['path'])): + for file in iter_folder_files(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) + 'token': get_direct_token(share, file), + 'name': file, + 'url': path2url(file) }) files.append(status) # direct share links not allowed if password isnt set @@ -227,6 +222,7 @@ def list_view(name, token = None): description = get_or_none('description', share, "") ) + @app.route('/logout/', methods=['GET']) def logout(name): if name in session: @@ -236,7 +232,8 @@ def logout(name): name = name ) -@app.route('/direct///', methods=['GET']) + +@app.route('/direct///', methods=['GET']) def download_direct(name,token,filename): (ok,share) = get_share(name, require_auth = False) if not ok: @@ -244,6 +241,8 @@ def download_direct(name,token,filename): allow_direct = get_or_none('direct_links', share) if allow_direct != True: return 'Direct download not allowed', 403 + if not is_path_safe(filename): + return 'Incorrect relative path'+filename, 403 file_token = get_direct_token(share, filename) if file_token == None: return 'Cannot generate token', 400 @@ -251,7 +250,7 @@ def download_direct(name,token,filename): return 'Incorrect token', 403 file_path = os.path.join(share['path'], filename) if not os.path.exists(file_path): - return 'no such file', 404 + return 'No such file', 404 notify({ "recipient": get_or_none('recipient', share), "share": name, @@ -261,22 +260,14 @@ def download_direct(name,token,filename): return send_from_directory(directory=share['path'], filename=filename) -@app.route('/download///', methods=['GET']) -@app.route('/download//', methods=['GET']) -def download_file(name, filename, token = None): - (ok,share) = get_share(name, token = token) - 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('recipient', share), - "share": name, - "filename": file_path, - "operation": "download" - }) - return send_from_directory(directory=share['path'], filename=filename) +@app.route('/download/gui//', methods=['GET']) +def download_gui(name, filename): + return download_file(name, filename, token = None) + + +@app.route('/download///', methods=['GET']) +def download_token(name, filename, token): + return download_file(name, filename, token = token) @app.route('/zip//', methods=['GET']) @@ -302,6 +293,7 @@ def download_zip(name, token = None): attachment_filename = name + ".zip" ) + @app.route('/script/upload//', methods=['GET']) def script_upload(name = None, token = None): (ok,share) = get_share(name, token = token) @@ -518,8 +510,8 @@ done token ) -class uploadJoiner: +class uploadJoiner: def __init__(self, target_name, parts): self.target_name = target_name self.parts = parts @@ -528,6 +520,7 @@ class uploadJoiner: p.daemon = True p.start() + def run(self): with open(self.target_name,'wb') as writer: for part in self.parts: @@ -539,6 +532,24 @@ class uploadJoiner: os.remove(part) +def download_file(name, filename, token = None): + (ok,share) = get_share(name, token = token) + if not ok: + return share + if not is_path_safe(filename): + return "Incorrect path", 403 + file_path = os.path.join(share['path'], filename) + if not os.path.exists(file_path): + return 'No such file, '+file_path, 404 + notify({ + "recipient": get_or_none('recipient', share), + "share": name, + "filename": file_path, + "operation": "download" + }) + return send_from_directory(directory=share['path'], filename=filename) + + def file_versionize(filename): """ Move file to versioned with integer """ stats = file_stat(filename) @@ -635,7 +646,6 @@ def set_rights(path): 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']) @@ -648,7 +658,7 @@ def zip_share(share): ) ) zf = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) - for file in sorted(os.listdir(share['path'])): + for file in iter_folder_files(share['path']): fp = os.path.join(share['path'], file) if os.path.isdir(fp): continue @@ -677,6 +687,7 @@ def zip_clean(): if __name__ == "__main__": + zip_clean() app.run(debug=True) diff --git a/code/flees-manager.py b/code/flees-manager.py index 17ac9c1..5426aba 100755 --- a/code/flees-manager.py +++ b/code/flees-manager.py @@ -6,6 +6,7 @@ from datetime import datetime from utils.utils import * from utils.crypt import * + def get_root_path(opts): root_folder = os.path.dirname( os.path.dirname( @@ -57,6 +58,7 @@ def list_folders(shares,config): for path, folders, files in os.walk(data_folder): full_path = os.path.join(data_folder, path) share_name = None + parent_is_share = False for share in shares: share_path = os.path.join(data_folder, share['path']) if not os.path.exists(share_path): @@ -64,6 +66,13 @@ def list_folders(shares,config): if os.path.samefile(full_path, share_path): share_name = share['name'] break + parents = full_path.split(os.sep) + for p in range(len(parents)): + test_path = os.sep+os.sep.join(parents[1:(p+2)]) + if os.path.samefile(test_path, share_path): + parent_is_share = True + if parent_is_share: + continue if share_name == None: # skip folder if it's not a share, and not a leaf if len(folders) > 0: @@ -83,6 +92,7 @@ def list_folders(shares,config): )) print(tabulate(table, headers = "firstrow")) + def add_share(shares, config, opts): # Make name and path safe: @@ -214,7 +224,6 @@ def modify_share(shares, config, opts): if not token in share['tokens']: share['tokens'].append(token) - if opts.expire: if opts.expire == "": # REMOVE EXPIRATION @@ -306,7 +315,6 @@ def show_share(shares, config, opts): print_share(share, config) - def print_rest_api(shares, config, opts): if 'public_url' not in config: print("Set public_url variable in your config.json") @@ -412,16 +420,12 @@ def print_rest_api_download(config, share, token): if not os.path.exists(share_path): print("no files") sys.exit(0) - for filename in sorted(os.listdir(share_path)): - if os.path.isdir(os.path.join(share_path,filename)): - continue - if filename.startswith("."): - continue + for filename in iter_folder_files(share_path): print("%s/download/%s/%s/%s"%( config['public_url'], share['name'], token, - filename + path2url(filename) )) print("or \n\n# curl -s %s/script/download/%s/%s | bash /dev/stdin [-f]"%( config['public_url'], @@ -444,16 +448,12 @@ def print_rest_api_direct(config, share, token): if not os.path.exists(share_path): print("no files") sys.exit(0) - for filename in sorted(os.listdir(share_path)): - if os.path.isdir(os.path.join(share_path,filename)): - continue - if filename.startswith("."): - continue + for filename in iter_folder_files(share_path): print("%s/direct/%s/%s/%s"%( config['public_url'], share['name'], get_direct_token(share,filename), - filename + path2url(filename) )) print("or \n\n# curl -s %s/script/direct/%s/%s | bash /dev/stdin [-f]"%( config['public_url'], diff --git a/code/templates/list.html b/code/templates/list.html index 4cae244..6ce80d7 100644 --- a/code/templates/list.html +++ b/code/templates/list.html @@ -60,7 +60,7 @@ {% if direct %} {% endif %} - {{ entry.name }} + {{ entry.name }} {{ entry.size|safe }} {{ entry.mtime|safe }} diff --git a/code/utils/utils.py b/code/utils/utils.py index 9d847fa..0e62871 100644 --- a/code/utils/utils.py +++ b/code/utils/utils.py @@ -1,7 +1,10 @@ import os from datetime import datetime from flask import current_app as app - +try: + from urllib.request import pathname2url +except ImportError: + from urllib import pathname2url def file_date_human(num): return datetime.fromtimestamp( @@ -47,12 +50,50 @@ def get_or_none(key,d,none = None): else: return none + +def is_path_safe(path): + if path.startswith("."): + return False + if "/." in path: + return False + return True + + +def iter_folder_files(path, recursive = True): + if recursive: + for dirpath, dirnames, filenames in os.walk(path, topdown = False): + relative_path = os.path.relpath(dirpath,path) + dirnames.sort() + if "/." in relative_path: + continue + if relative_path == ".": + relative_path = "" + for f in sorted(filenames): + if f.startswith("."): + continue + fp = os.path.join(relative_path, f) + yield fp + else: + for file in sorted(path): + fp = os.path.join(path,file) + if os.path.isdir(fp): + continue + if file.startswith("."): + continue + yield fp + + +def path2url(path): + return pathname2url(path) + def safe_name(s): return safe_string(s, "-_") + def safe_path(s): return safe_string(s, "-_/") + def safe_string(s, valid): """ return a safe string, replace non alnum characters with _ . all characters in valid are considered valid. """ return "".join([c if c.isalnum() or c in valid else "_" for c in s])