binbin/binbin.py

461 lines
11 KiB
Python

import sys, os, time, hashlib, uuid, threading, atexit, flask, \
configparser as cp
from flask import Flask, render_template, url_for, request, session, \
send_from_directory, send_file, redirect
from flask_pymongo import PyMongo, ObjectId
FILTERS = {
'name': 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' +
'0123456789_',
'path': ['../', './']
}
def apply_config():
config = cp.ConfigParser()
config.read('config.ini')
for k,v in config['Settings'].items():
k = k.upper()
if k == 'KEY_FILE':
app.secret_key = open(v, 'rb').read()
elif k == 'DEFAULT_VIRTUAL_SIZE':
app.config[k] = int(v)
else:
app.config[k] = v
os.chdir(os.path.dirname(__file__))
app = Flask(__name__)
apply_config()
mongo = PyMongo(app)
USERS, DRIVES, LINKS = mongo.db['users'], mongo.db['drives'], \
mongo.db['links']
CHECK_PERIOD = 60
expire_thread = threading.Thread()
@app.route('/')
def index():
if 'username' in session:
return render_template('pre_main.html')
else:
return render_template('index.html')
@app.route('/main/<method>')
def main(method):
return render_template(method+'.html')
@app.route('/login', methods=['POST'])
def login():
error = None
u, p = request.form['username'], request.form['password']
success = validate_login(u, p)
if success:
session['username'] = u
return flask.jsonify(True)
else:
return flask.jsonify(False)
@app.route('/logout')
def logout():
session.pop('username', None)
return redirect(url_for('index'))
@app.route('/mydrives')
def mydrives():
if 'username' not in session:
return redirect(url_for('index'))
user_id = get_user(session)['_id']
owned = DRIVES.find({'owner': user_id})
shared = DRIVES.find({'shared_read': {'$in': [user_id]}})
info = {'owned': [], 'shared': []}
for drive in owned:
drive_info = {
'_id': str(drive['_id']),
'name': drive['name'],
'size': drive['size'],
'public_read': drive['public_read'],
'public_write': drive['public_write'],
'shared_read': [str(i) for i in drive['shared_read']],
'shared_write': [str(i) for i in drive['shared_write']]
}
info['owned'].append(drive_info)
for drive in shared:
drive_info = {
'_id': str(drive['_id']),
'name': drive['name'],
'size': drive['size']
}
info['shared'].append(drive_info)
return flask.jsonify(info)
@app.route('/files', methods=['POST'])
def files():
check = verify_data('files', request.form, session)
if not check[0]: return check[1]
form = check[1]
if form['is_fol']: # If request is a folder.
info = dir_info(form['path'], form['drive']['type'], \
form['drive']['_id'])
return flask.jsonify(info)
else: # If request is a file.
if form['drive']['type'] == 'real':
link = LINKS.insert_one({
'path': form['path'],
'name': form['path'].split("/")[-1],
'shared': [get_user(session)['_id']],
'expiry': -1
}).inserted_id
elif form['drive']['type'] == 'virtual':
r_path = form['drive']['path'] + '/' + form['real_file']
link = LINKS.insert_one({
'path': r_path,
'name': form['path'].split("/")[-1],
'shared': [get_user(session)['_id']],
'expiry': -1
}).inserted_id
else:
raise Exception("Invalid method for file link creation.")
### QUEUE DELETION
return str(link)
@app.route('/d/<_id>')
def download(_id):
if 'username' not in session:
return redirect(url_for('index'))
try:
link = LINKS.find_one({'_id': ObjectId(_id)})
except:
return redirect(url_for('index'))
if link == None: return redirect(url_for('index'))
if get_user(session)['_id'] not in link['shared']:
return redirect(url_for('index'))
else:
if link['expiry'] == -1:
LINKS.delete_one({'_id': ObjectId(_id)})
return send_file(link['path'], as_attachment=True,
attachment_filename=link['name'])
@app.route('/users/<method>', methods=['POST'])
def users(method):
if 'username' not in session:
return redirect(url_for('index'))
user = USERS.find_one({'username': session['username']})
if method == 'create':
check = verify_data('users.create', request.form, session)
if not check[0]: return check[1]
form = check[1]
salt = uuid.uuid4().hex
to_hash = (form['password'] + salt).encode('utf-8')
user = USERS.insert_one({
'username': form['username'],
'password': hashlib.sha512(to_hash).digest(),
'salt': salt,
'perm_level': 1
})
create_drive('virtual', user.inserted_id)
elif method == 'delete':
check = verify_data('users.delete', request.form, session)
if not check[0]: return check[1]
form = check[1]
USERS.delete_one({'username': form['username']})
elif method == 'modify':
pass
return 'Operation completed'
@app.route('/drive/<drive_id>/<path:path>')
def drive_path():
pass
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
# Temporary for development:
@app.route('/css/<path:path>')
def css(path):
return send_from_directory('css', path)
@app.route('/js/<path:path>')
def js(path):
return send_from_directory('js', path)
@app.route('/assets/<path:path>')
def assets(path):
return send_from_directory('assets', path)
# End temporary
def get_user(sess):
return USERS.find_one({'username': session['username']})
def validate_login(u, p):
user = USERS.find_one({'username': u})
if user == None:
return False
else:
to_hash = (p + user['salt']).encode('utf-8')
return hashlib.sha512(to_hash).digest() == user['password']
def sizeof_fmt(num, suffix='B'):
for unit in ['','K','M','G','T','P','E','Z']:
if abs(num) < 1024.0:
return '%3.1f%s%s' % (num, unit, suffix)
num /= 1024.0
return '%.1f%s%s' % (num, 'Y', suffix)
def dir_info(path, t, drive_id):
def info_dict(item, stats, is_fol):
if is_fol:
my_item = ({
'folder': True,
'name': item
})
else:
my_item = ({
'folder': False,
'name': item,
'date': time.strftime('%Y-%m-%d %H:%M:%S UTC', \
time.gmtime(stats[8])),
'size': sizeof_fmt(stats[6]),
'real_size': stats[6]
})
return my_item
full_items = []
if t == 'real':
items = os.listdir(path)
for item in items:
stats = list(os.stat(path + '/' + item))
is_fol = os.path.isdir(path + '/' + item)
full_items.append(info_dict(item, stats, is_fol))
elif t == 'virtual':
drives = DRIVES.find_one({'_id': drive_id})
tree = drives['tree']
path = path.replace('.',':').split("/")[1:]
if path != []:
for sub in path: tree = tree[sub]
for k,v in tree.items():
is_fol = type(v).__name__ != 'str'
if is_fol:
stats = None
else:
stats = list(os.stat(drives['path'] + '/' + v))
full_items.append(info_dict(k.replace(':','.'), \
stats, is_fol))
return full_items
def exists(data, need):
try:
[i in data for i in need].index(False)
return False
except ValueError:
return True
def sanitize(d):
if 'username' in d:
s = ''
for c in d['username']: s += c if c in FILTERS['name'] else ''
d['username'] = s
if 'path' in d:
for s in FILTERS['path']:
if s in d['path']:
d['path'] = ''
def sess_is_user(sess, name):
return sess['username'] == name
def sess_is_admin(sess):
return get_user(sess)['perm_level'] == 100
def copy_dict(form):
new_form = {}
for k,v in form.items(): new_form[k] = v
return new_form
def verify_data(method, form, sess):
'''
Verifies permissions and format and sanitizes user input.
For each method, 1) Check for malformed data. 3) Check permissions
for operation based on session. 3) Sanitize data. 4) Check
operation specific requirements.
'''
err_msgs = {
'data': 'malformed data',
'permission': 'insufficient permissions',
'userexists': 'username already in use',
'driveperm': 'the drive is not shared with you',
'pathinvalid': 'not a valid path'
}
errors = []
data = copy_dict(form)
if method == 'users.create':
has_items = exists(data, ['username', 'password'])
if not has_items: errors.append('data')
if not sess_is_admin(sess): errors.append('permission')
sanitize(data)
try:
if USERS.find_one({'username': data['username']}) != None:
errors.append('userexists')
except KeyError:
pass
elif method == 'users.delete':
has_items = exists(data, ['username'])
if not has_items: errors.append('data')
if not sess_is_admin(sess) and \
not sess_is_user(sess, data['username']):
return errors.append('permission')
sanitize(data)
elif method == 'users.modify':
pass
elif method == 'files':
has_items = exists(data, ['drive_id', 'path'])
if not has_items: errors.append('data')
drive = DRIVES.find_one({'_id': ObjectId(data['drive_id'])})
if drive == None: errors.append('driveinvalid')
if not drive['public_read']:
user_id = get_user(sess)['_id']
if user_id != drive['owner'] and \
user_id not in drive['shared_read']:
errors.append('driveperm')
sanitize(data)
if drive['type'] == 'real':
if not os.path.exists(drive['path'] + data['path']):
errors.append('pathinvalid')
data['is_fol'] = os.path.isdir(drive['path'] + \
data['path'])
# For real drives, the path is kept as the full real path.
data['path'] = drive['path']+data['path']
elif drive['type'] == 'virtual':
tree = drive['tree']
path = data['path'].replace('.',':').split('/')[1:]
if path != []:
folder, f = path[:-1], path[-1]
try:
for sub in folder: tree = tree[sub]
data['is_fol'] = type(tree[f]).__name__ != 'str'
data['real_file'] = tree[f]
except KeyError:
errors.append('pathinvalid')
else:
data['is_fol'] = True
# For virtual drives, the path is just the user request.
data['drive'] = drive
else:
raise Exception('Invalid data verification method.')
if len(errors) == 0: return [True, data]
msg = 'Error: '
for i,e in enumerate(errors):
if len(errors) == 1:
msg += err_msgs[e][0].upper() + err_msgs[e][1:]
elif i == 0:
msg += err_msgs[e][0].upper() + err_msgs[e][1:] + ', '
elif i == len(errors)-1:
msg += 'and ' + err_msgs[e]
else:
msg += err_msgs[e] + ', '
msg += '.'
if len(errors) <= 2: msg = msg.replace(',', '')
return [False, msg]
def create_drive(method, owner, form=None):
if method == 'real':
DRIVES.insert_one({
'name': form['name'],
'path': form['path'],
'type': 'real',
'owner': owner,
'public_read': False,
'public_write': False,
'shared_read': [],
'shared_write': []
})
elif method == 'virtual':
vir_path = app.config['VIRTUAL_DB_PATH'] + "/" + str(owner)
name = str(uuid.uuid4())+str(uuid.uuid4())
DRIVES.insert_one({
'name': 'My Drive',
'path': vir_path,
'tree': {
'Welcome:txt': name
},
'type': 'virtual',
'size': app.config['DEFAULT_VIRTUAL_SIZE'],
'owner': owner,
'public_read': False,
'public_write': False,
'shared_read': [],
'shared_write': []
})
os.mkdir(vir_path)
file = open(vir_path+'/'+name, 'w')
file.write(app.config['WELCOME_MSG'])
file.close()
else:
raise Exception('Invalid drive creation method.')
return 'Operation completed.'
def manage_expiry():
links = LINKS.find()
#print(links)
if __name__ == '__main__':
app.run(debug=True)