reborked the token system

This commit is contained in:
Ville Rantanen
2018-03-01 15:05:29 +02:00
parent 9ed8c18fb3
commit 06d42ee956
4 changed files with 241 additions and 152 deletions

View File

@@ -71,8 +71,7 @@ Operation is one of download, direct_download, zip_download, or upload
- shares.json stores hashed version of password. - shares.json stores hashed version of password.
- Additionally, it may store plain text password, if users so wish. - Additionally, it may store plain text password, if users so wish.
- Internally, Flees only compares the hashes of passwords - Internally, Flees only compares the hashes of passwords
- Tokens are encrypted versions of the hash. (login/upload/download with - Tokens are secret strings that allow login/upload/download with
direct links). i.e. decrypted URL request equals password hash direct links. You can have many tokens for single share.
- Encryption key is the app_secret_key
- Direct download token is (password hash + filename) hashed - Direct download token is (password hash + filename) hashed

View File

@@ -40,7 +40,6 @@ if 'notifier' in config_values:
app.secret_key = config_values['app_secret_key'] app.secret_key = config_values['app_secret_key']
app.wsgi_app = ReverseProxied(app.wsgi_app) app.wsgi_app = ReverseProxied(app.wsgi_app)
app.config['CRYPTO'] = Crypto(app.secret_key)
@app.before_request @app.before_request
def before_request(): def before_request():
@@ -80,16 +79,14 @@ def authenticate(name):
session[name] = password_hash(user_password, app.secret_key) session[name] = password_hash(user_password, app.secret_key)
return redirect(url_for('list_view',name=name)) return redirect(url_for('list_view',name=name))
@app.route('/upload/<name>/<password>', methods=['POST']) @app.route('/upload/<name>/<token>', methods=['POST'])
@app.route('/upload', methods=['POST']) @app.route('/upload', methods=['POST'])
def upload(name = None, password = None): def upload(name = None, token = None):
if request.method == 'POST': if request.method == 'POST':
file = request.files['file'] file = request.files['file']
if name == None: if name == None:
name = request.form['name'] name = request.form['name']
if password != None: (ok,share) = get_share(name, token = token)
session[name] = app.config['CRYPTO'].decrypt(password)
(ok,share) = get_share(name)
if not ok: if not ok:
return share return share
if not get_or_none('upload', share) == True: if not get_or_none('upload', share) == True:
@@ -121,11 +118,10 @@ def upload(name = None, password = None):
return "Use the 'file' variable to upload\n",400 return "Use the 'file' variable to upload\n",400
@app.route('/upload_join/<name>/<password>', methods=['POST']) @app.route('/upload_join/<name>/<token>', methods=['POST'])
def upload_join_splitted(name, password): def upload_join_splitted(name, token):
if request.method == 'POST': if request.method == 'POST':
session[name] = app.config['CRYPTO'].decrypt(password) (ok,share) = get_share(name, token = token)
(ok,share) = get_share(name)
if not ok: if not ok:
return share return share
if not get_or_none('upload', share) == True: if not get_or_none('upload', share) == True:
@@ -173,15 +169,16 @@ def send(name):
return share return share
return render_template('send.html',name=name) return render_template('send.html',name=name)
@app.route('/list/<name>/<password>', methods=['GET']) @app.route('/list/<name>/<token>', methods=['GET'])
@app.route('/list/<name>', methods=['GET']) @app.route('/list/<name>', methods=['GET'])
def list_view(name, password = None): def list_view(name, token = None):
if password != None: (ok,share) = get_share(name, token = token)
session[name] = app.config['CRYPTO'].decrypt(password)
return redirect(url_for('list_view',name=name))
(ok,share) = get_share(name)
if not ok: if not ok:
return share return share
if token != None and 'pass_hash' in share:
session[name] = share['pass_hash']
return redirect(url_for('list_view',name=name))
files = [] files = []
for file in sorted(os.listdir(share['path'])): for file in sorted(os.listdir(share['path'])):
fp = os.path.join(share['path'],file) fp = os.path.join(share['path'],file)
@@ -247,12 +244,10 @@ def download_direct(name,token,filename):
return send_from_directory(directory=share['path'], filename=filename) return send_from_directory(directory=share['path'], filename=filename)
@app.route('/download/<name>/<password>/<filename>', methods=['GET']) @app.route('/download/<name>/<token>/<filename>', methods=['GET'])
@app.route('/download/<name>/<filename>', methods=['GET']) @app.route('/download/<name>/<filename>', methods=['GET'])
def download_file(name,filename,password = None): def download_file(name, filename, token = None):
if password != None: (ok,share) = get_share(name, token = token)
session[name] = app.config['CRYPTO'].decrypt(password)
(ok,share) = get_share(name)
if not ok: if not ok:
return share return share
file_path = os.path.join(share['path'], filename) file_path = os.path.join(share['path'], filename)
@@ -267,12 +262,10 @@ def download_file(name,filename,password = None):
return send_from_directory(directory=share['path'], filename=filename) return send_from_directory(directory=share['path'], filename=filename)
@app.route('/zip/<name>/<password>', methods=['GET']) @app.route('/zip/<name>/<token>', methods=['GET'])
@app.route('/zip/<name>', methods=['GET']) @app.route('/zip/<name>', methods=['GET'])
def download_zip(name,password = None): def download_zip(name, token = None):
if password != None: (ok,share) = get_share(name, token = token)
session[name] = app.config['CRYPTO'].decrypt(password)
(ok,share) = get_share(name)
if not ok: if not ok:
return share return share
folder_size = get_folder_size(share['path']) folder_size = get_folder_size(share['path'])
@@ -292,10 +285,9 @@ def download_zip(name,password = None):
attachment_filename = name + ".zip" attachment_filename = name + ".zip"
) )
@app.route('/script/upload/<name>/<password>', methods=['GET']) @app.route('/script/upload/<name>/<token>', methods=['GET'])
def script_upload(name = None, password = None): def script_upload(name = None, token = None):
session[name] = app.config['CRYPTO'].decrypt(password) (ok,share) = get_share(name, token = token)
(ok,share) = get_share(name)
if not ok: if not ok:
return share return share
if not get_or_none('upload', share) == True: if not get_or_none('upload', share) == True:
@@ -335,14 +327,13 @@ done
"""%( """%(
request.url_root, request.url_root,
name, name,
password token
) )
@app.route('/script/download/<name>/<password>', methods=['GET']) @app.route('/script/download/<name>/<token>', methods=['GET'])
def script_download(name = None, password = None): def script_download(name = None, token = None):
session[name] = app.config['CRYPTO'].decrypt(password) (ok,share) = get_share(name, token = token)
(ok,share) = get_share(name)
if not ok: if not ok:
return share return share
files = [] files = []
@@ -385,7 +376,7 @@ get_file() {
"""%( """%(
request.url_root, request.url_root,
name, name,
password token
) )
for file in files: for file in files:
@@ -395,10 +386,9 @@ get_file() {
return script return script
@app.route('/script/direct/<name>/<password>', methods=['GET']) @app.route('/script/direct/<name>/<token>', methods=['GET'])
def script_direct(name = None, password = None): def script_direct(name = None, token = None):
session[name] = app.config['CRYPTO'].decrypt(password) (ok,share) = get_share(name, token = token)
(ok,share) = get_share(name)
if not ok: if not ok:
return share return share
files = [] files = []
@@ -451,10 +441,9 @@ get_file() {
return script return script
@app.route('/script/upload_split/<name>/<password>', methods=['GET']) @app.route('/script/upload_split/<name>/<token>', methods=['GET'])
def script_upload_split(name = None, password = None): def script_upload_split(name = None, token = None):
session[name] = app.config['CRYPTO'].decrypt(password) (ok,share) = get_share(name, token = token)
(ok,share) = get_share(name)
if not ok: if not ok:
return share return share
if not get_or_none('upload', share) == True: if not get_or_none('upload', share) == True:
@@ -509,7 +498,7 @@ done
"""%( """%(
request.url_root, request.url_root,
name, name,
password token
) )
class uploadJoiner: class uploadJoiner:
@@ -554,7 +543,7 @@ def file_versionize(filename):
os.rename(filename,new_name) os.rename(filename,new_name)
def get_share(name, require_auth = True): def get_share(name, require_auth = True, token = None):
share = [x for x in g.shares if x['name'] == name] share = [x for x in g.shares if x['name'] == name]
if len(share) < 1: if len(share) < 1:
return (False,redirect(url_for('authenticate',name=name))) return (False,redirect(url_for('authenticate',name=name)))
@@ -562,6 +551,10 @@ def get_share(name, require_auth = True):
if is_expired(share): if is_expired(share):
return (False, 'Share has expired') return (False, 'Share has expired')
authenticated = "no-pass" authenticated = "no-pass"
if not token == None:
if has_token(token, share):
require_auth = False
authenticated = "token"
if require_auth: if require_auth:
if 'pass_hash' in share: if 'pass_hash' in share:
authenticated = False authenticated = False
@@ -584,6 +577,7 @@ def get_share(name, require_auth = True):
makedirs_rights(share['path']) makedirs_rights(share['path'])
return (True,share) return (True,share)
def is_expired(share): def is_expired(share):
expires = get_or_none('expire', share) expires = get_or_none('expire', share)
if expires: if expires:
@@ -607,6 +601,7 @@ def makedirs_rights(path):
os.mkdir(current_path) os.mkdir(current_path)
set_rights(current_path) set_rights(current_path)
def notify(msg): def notify(msg):
if 'notifier' in app.config: if 'notifier' in app.config:
msg['environment'] = request.environ msg['environment'] = request.environ

View File

@@ -19,20 +19,20 @@ def get_root_path(opts):
def list_shares(shares,opts): def list_shares(shares,opts):
table = [] table = []
table.append(('Name', 'Path','Public','Password','PassHash','Upload','Overwrite','Direct','Expire','Recipient','Description')) table.append(('Name', 'Path','Public','Password','Tokens','Upload','Overwrite','Direct','Expire','Recipient','Description'))
for share in shares: for share in shares:
public = get_or_none('public',share, False) public = get_or_none('public',share, False)
passhash = '-' passtoken = '-'
password = 'pass_hash' in share password = 'pass_hash' in share
if opts.show_password: if opts.show_password:
if 'pass_plain' in share: if 'pass_plain' in share:
password = share['pass_plain'] password = share['pass_plain']
else: else:
password = "" password = ""
if 'pass_hash' in share: if 'tokens' in share:
passhash = share['pass_hash'] passtoken = ",".join(share['tokens'])
else: else:
passhash = "-" passtoken = ""
upload = get_or_none('upload',share, False) upload = get_or_none('upload',share, False)
overwrite = get_or_none('overwrite',share, True) overwrite = get_or_none('overwrite',share, True)
direct = get_or_none('direct_links',share, False) if password else False direct = get_or_none('direct_links',share, False) if password else False
@@ -43,7 +43,7 @@ def list_shares(shares,opts):
share['path']+"/", share['path']+"/",
public, public,
password, password,
passhash, passtoken,
upload, upload,
overwrite, overwrite,
direct, direct,
@@ -102,6 +102,7 @@ def add_share(shares, config, opts):
if opts.plain: if opts.plain:
share['pass_plain'] = opts.password share['pass_plain'] = opts.password
share['pass_hash'] = password_hash(opts.password, config['app_secret_key']) share['pass_hash'] = password_hash(opts.password, config['app_secret_key'])
share['tokens'] = [random_token()]
if opts.expire: if opts.expire:
try: try:
date_object = datetime.strptime(opts.expire,"%Y-%m-%d %H:%M") date_object = datetime.strptime(opts.expire,"%Y-%m-%d %H:%M")
@@ -138,7 +139,9 @@ def modify_share(shares, config, opts):
for i,share in enumerate(shares): for i,share in enumerate(shares):
if share['name'] != opts.name: if share['name'] != opts.name:
continue continue
orig_share = share.copy() orig_share = dict(share)
if 'tokens' in share:
orig_share['tokens'] = list(share['tokens'])
print(json.dumps(share, indent = 2, sort_keys = True)) print(json.dumps(share, indent = 2, sort_keys = True))
found = True found = True
break break
@@ -167,6 +170,19 @@ def modify_share(shares, config, opts):
share['pass_plain'] = opts.password share['pass_plain'] = opts.password
share['pass_hash'] = password_hash(opts.password, config['app_secret_key']) share['pass_hash'] = password_hash(opts.password, config['app_secret_key'])
# Handle tokens
if opts.remove_tokens:
for token in opts.remove_tokens:
if token in share['tokens']:
share['tokens'].remove(token)
if opts.tokens:
for token in opts.tokens:
if not 'tokens' in share:
share['tokens'] = []
if not token in share['tokens']:
share['tokens'].append(token)
if opts.expire: if opts.expire:
if opts.expire == "": if opts.expire == "":
# REMOVE EXPIRATION # REMOVE EXPIRATION
@@ -201,7 +217,7 @@ def modify_share(shares, config, opts):
if not key in orig_share: if not key in orig_share:
modified.append(key) modified.append(key)
continue continue
if orig_share[key] != share[key]: if str(orig_share[key]) != str(share[key]):
modified.append(key) modified.append(key)
continue continue
for key in orig_share: for key in orig_share:
@@ -242,48 +258,91 @@ def print_rest_api(shares, config, opts):
share = shares[0] share = shares[0]
if opts.type == "list": if opts.type == "list":
print_rest_api_list(config,share)
return
if (not 'tokens' in share) or len(share['tokens']) == 0:
print("REST API enabled only if tokens are set for the share")
sys.exit(1)
token = False
if len(share['tokens']) == 1:
token = share['tokens'][0]
else:
try:
token_int = int(opts.token) - 1
if token_int < 0:
raise ValueError
token = share['tokens'][token_int]
except (IndexError, ValueError, TypeError) as e:
if opts.token in share['tokens']:
token = opts.token
if not token:
# more tokens!
if opts.token:
print("No such token for this share")
print("Tokens:")
for i,token in enumerate(share['tokens']):
print("%d. %s"%( i+1, token ))
print("Run again with --token [nr]")
if not opts.token:
sys.exit(0)
else:
sys.exit(1)
if opts.type == "login":
print_rest_api_login(config,share,token)
elif opts.type == "upload":
print_rest_api_upload(config,share,token)
elif opts.type == "download":
print_rest_api_download(config, share, token)
elif opts.type == "direct":
print_rest_api_direct(config, share, token)
elif opts.type == "zip":
print_rest_api_zip(config, share, token)
def print_rest_api_list(config, share):
print("Link to enter the share:") print("Link to enter the share:")
print("%s/list/%s"%( print("%s/list/%s"%(
config['public_url'], config['public_url'],
share['name'] share['name']
)) ))
return
if not 'pass_hash' in share:
print("REST API enabled only if pass_hash is set for share") def print_rest_api_login(config, share, token):
sys.exit(1)
crypter = Crypto(config['app_secret_key'])
crypted = crypter.encrypt(share['pass_hash'])
if opts.type == "login":
print("Link to automatically login in the share:") print("Link to automatically login in the share:")
print("%s/list/%s/%s"%( print("%s/list/%s/%s"%(
config['public_url'], config['public_url'],
share['name'], share['name'],
crypted token
)) ))
elif opts.type == "upload":
def print_rest_api_upload(config, share, token):
if 'upload' not in share or not share['upload']: if 'upload' not in share or not share['upload']:
print("Uploading not allowed to this share") print("Uploading not allowed to this share")
sys.exit(0) sys.exit(0)
print("Link to upload file to the share:") print("Link to upload file to the share:")
print("\n# curl -F file=@'the_file_name.ext' %s/upload/%s/%s"%( print("\n# curl -F file=@'the_file_name.ext' %s/upload/%s/%s"%(
config['public_url'], config['public_url'],
share['name'], share['name'],
crypted token
)) ))
print("\nLink to upload multiple files to the share:") print("\nLink to upload multiple files to the share:")
print("\n# curl -s %s/script/upload/%s/%s | bash /dev/stdin file_to_upload.ext [second.file.ext]"%( print("\n# curl -s %s/script/upload/%s/%s | bash /dev/stdin file_to_upload.ext [second.file.ext]"%(
config['public_url'], config['public_url'],
share['name'], share['name'],
crypted token
)) ))
print("\nLink to upload multiple files to the share, splitting large files (default 512Mb):") print("\nLink to upload multiple files to the share, splitting large files (default 512Mb):")
print("\n# curl -s %s/script/upload_split/%s/%s | bash /dev/stdin [-s split_size_in_Mb] file_to_upload.ext [second.file.ext]"%( print("\n# curl -s %s/script/upload_split/%s/%s | bash /dev/stdin [-s split_size_in_Mb] file_to_upload.ext [second.file.ext]"%(
config['public_url'], config['public_url'],
share['name'], share['name'],
crypted token
)) ))
elif opts.type == "download":
def print_rest_api_download(config, share, token):
print("Links to download files:") print("Links to download files:")
share_path = os.path.join( share_path = os.path.join(
config['__root_path__'], config['__root_path__'],
@@ -301,15 +360,18 @@ def print_rest_api(shares, config, opts):
print("%s/download/%s/%s/%s"%( print("%s/download/%s/%s/%s"%(
config['public_url'], config['public_url'],
share['name'], share['name'],
crypted, token,
filename filename
)) ))
print("or \n\n# curl -s %s/script/download/%s/%s | bash /dev/stdin [-f]"%( print("or \n\n# curl -s %s/script/download/%s/%s | bash /dev/stdin [-f]"%(
config['public_url'], config['public_url'],
share['name'], share['name'],
crypted token
)) ))
elif opts.type == "direct":
def print_rest_api_direct(config, share, token):
if 'direct_links' not in share or not share['direct_links']: if 'direct_links' not in share or not share['direct_links']:
print("Direct downloading not allowed in this share") print("Direct downloading not allowed in this share")
sys.exit(0) sys.exit(0)
@@ -336,17 +398,23 @@ def print_rest_api(shares, config, opts):
print("or \n\n# curl -s %s/script/direct/%s/%s | bash /dev/stdin [-f]"%( print("or \n\n# curl -s %s/script/direct/%s/%s | bash /dev/stdin [-f]"%(
config['public_url'], config['public_url'],
share['name'], share['name'],
crypted token
)) ))
elif opts.type == "zip":
def print_rest_api_zip(config, share, token):
print("ZIP download:") print("ZIP download:")
print("%s/zip/%s/%s"%( print("%s/zip/%s/%s"%(
config['public_url'], config['public_url'],
share['name'], share['name'],
crypted token
)) ))
def print_token():
print(random_token())
def parse_options(): def parse_options():
config_default = os.path.realpath( config_default = os.path.realpath(
os.path.join( os.path.join(
@@ -431,6 +499,12 @@ def parse_options():
parser_modify.add_argument('-r','--recipient', action="store", dest="recipient", default = None, parser_modify.add_argument('-r','--recipient', action="store", dest="recipient", default = None,
help= "Recipient for notifications (if enabled)" help= "Recipient for notifications (if enabled)"
) )
parser_modify.add_argument('-t','--add-token', action="append", dest="tokens", default = [],
help= "Token for REST api, may be issued multiple times"
)
parser_modify.add_argument('--remove-token', action="append", dest="remove_tokens", default = [],
help= "Remove REST tokens, may be issued multiple times"
)
parser_modify.add_argument('-w','--write', action="store_true", dest="write", default = False, parser_modify.add_argument('-w','--write', action="store_true", dest="write", default = False,
help = "Write changes to the shares.json file" help = "Write changes to the shares.json file"
) )
@@ -440,6 +514,11 @@ def parse_options():
parser_rest.add_argument(dest="type", help = "Type of command", parser_rest.add_argument(dest="type", help = "Type of command",
choices = ['list','login','upload','download','direct','zip'] choices = ['list','login','upload','download','direct','zip']
) )
parser_rest.add_argument('-t','--token', action="store", dest="token", default = None,
help= "If share has multiple tokens, select one to print REST API for."
)
## TOKEN
parser_token = subparsers.add_parser('token', help = "Generate a random token")
return parser.parse_args() return parser.parse_args()
@@ -477,6 +556,8 @@ if __name__ == "__main__":
modify_share(shares,config,opts) modify_share(shares,config,opts)
elif opts.subparser_name == 'rest': elif opts.subparser_name == 'rest':
print_rest_api(shares,config,opts) print_rest_api(shares,config,opts)
elif opts.subparser_name == 'token':
print_token()

View File

@@ -1,3 +1,5 @@
import random
import string
import base64 import base64
from Crypto.Cipher import AES from Crypto.Cipher import AES
import hashlib import hashlib
@@ -44,6 +46,12 @@ def get_direct_token(share, filename):
) )
def has_token(token, share):
if not 'tokens' in share:
return False
return token in share['tokens']
def password_hash(string, salt=""): def password_hash(string, salt=""):
if type(string) == str: if type(string) == str:
string = string.encode("utf-8") string = string.encode("utf-8")
@@ -54,6 +62,12 @@ def password_hash(string, salt=""):
).hexdigest() ).hexdigest()
def random_token():
chars = [random.choice(string.ascii_letters + string.digits) for n in range(30)]
token = "".join(chars)
return token
def remove_pad(string): def remove_pad(string):
""" Remove spaces from right """ """ Remove spaces from right """
return string.rstrip(" ") return string.rstrip(" ")