CloudStore.py 12.9 KB
Newer Older
1 2
#!/usr/bin/python

3
# TODO File permission checks
4

5
from bottle import route, run, request, static_file, abort, redirect, app, response
6 7 8
import json, os, shutil
import uuid
import subprocess
9
import ConfigParser
10
from pwd import getpwnam
11
import multiprocessing
12

13
# Get configuration file
14
config = ConfigParser.ConfigParser()
tarokkk committed
15
config.read('/opt/webadmin/cloud/miscellaneous/store-server/store.config')
16 17 18 19 20 21


ROOT_WWW_FOLDER = config.get('store', 'root_www_folder')
ROOT_BIN_FOLDER = config.get('store', 'root_bin_folder')
SITE_URL = config.get('store', 'site_url')
USER_MANAGER = config.get('store', 'user_manager')
22
# Standalone server
23 24
SITE_HOST = config.get('store', 'site_host')
SITE_PORT = config.get('store', 'site_port')
25
# Temporary dir for tar.gz
26
TEMP_DIR = config.get('store', 'temp_dir')
27 28 29 30 31
#Redirect
try:
    REDIRECT_URL = config.get('store', 'redirect_url')
except:
    REDIRECT_URL = "https://cloud.ik.bme.hu"
32 33 34 35 36 37 38 39 40
#ForceSSL
try:
    FORCE_SSL = config.get('store', 'force_ssl') == "True"
except:
    FORCE_SSL = False


def force_ssl(original_function):
    def new_function(*args, **kwargs):
41 42 43 44 45 46
        if FORCE_SSL:
            ssl = request.environ.get('SSL_CLIENT_VERIFY', 'NONE')
            if ssl != "SUCCESS":
                abort(403, "Forbidden requests. This site need SSL verification! SSL status: "+ssl)
            else:
                return original_function(*args, **kwargs)
47 48
        else:
            return original_function(*args, **kwargs)
49
    return new_function
50 51

@route('/')
52
@force_ssl
53
def index():
54 55
    response = "NONE"
    try:
56
        response = request.environ.get('SSL_CLIENT_VERIFY', 'NONE')
57 58 59
    except:
        pass
    return "It works! SSL: "+response
60

61
# @route('/<neptun:re:[a-zA-Z0-9]{6}>', method='GET')
62
@route('/<neptun>', method='GET')
63
@force_ssl
64 65 66 67 68
def neptun_GET(neptun):
    home_path = '/home/'+neptun+'/home'
    if os.path.exists(home_path) != True:
        abort(401, 'The requested user does not exist!')
    else:
69
        statistics=get_quota(neptun)
70 71
        return { 'Used' : statistics[0], 'Soft' : statistics[1], 'Hard' : statistics[2]}

72 73
COMMANDS = {}

74
@route('/<neptun>', method='POST')
75
@force_ssl
76
def neptun_POST(neptun):
77
    # Check if user avaiable (home folder ready)
78 79 80 81
    home_path = '/home/'+neptun+'/home'
    if os.path.exists(home_path) != True:
        abort(401, 'The requested user does not exist!')
    else:
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
        try:
            return COMMANDS[request.json['CMD']](request, neptun, home_path)
        except KeyError:
            abort(400, "Command not found!")


# LISTING
def cmd_list(request, neptun, home_path):
    list_path = home_path+request.json['PATH']
    if os.path.exists(list_path) != True:
        abort(404, "Path not found!")
    else:
        return list_directory(home_path, list_path)
COMMANDS['LIST'] = cmd_list

# DOWNLOAD LINK GENERATOR
def cmd_download(request, neptun, home_path):
    dl_path = home_path+'/'+request.json['PATH']
    dl_path = os.path.realpath(dl_path)
    if not dl_path.startswith(home_path):
        abort(400, 'Invalid download path.')
    dl_hash = str(uuid.uuid4())
104 105 106
    dl_pub = os.path.join(ROOT_WWW_FOLDER, dl_hash)
    if os.path.isfile(dl_path):
        os.symlink(dl_path, dl_pub)
107 108
        return json.dumps({'LINK' : SITE_URL+'/dl/'+dl_hash})
    else:
109 110
        shutil.make_archive(dl_pub, 'zip', dl_path)
        return json.dumps({'LINK' : SITE_URL+'/dl/'+dl_hash+'.zip'})
111 112 113 114 115 116 117 118 119 120
COMMANDS['DOWNLOAD'] = cmd_download

# UPLOAD
def cmd_upload(request, neptun, home_path):
    up_path = home_path+'/'+request.json['PATH']
    up_path = os.path.realpath(up_path)
    if not up_path.startswith(home_path):
        abort(400, 'Invalid upload path.')
    if os.path.exists(up_path) == True and os.path.isdir(up_path):
        up_hash = str(uuid.uuid4())
121 122 123 124
        link = ROOT_WWW_FOLDER + '/' + up_hash
        os.symlink(up_path, link)
        passwd = getpwnam(neptun)
        os.lchown(link, passwd.pw_uid, passwd.pw_gid)
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
        return json.dumps({ 'LINK' : SITE_URL+'/ul/'+up_hash})
    else:
        abort(400, 'Upload directory not exists!')
COMMANDS['UPLOAD'] = cmd_upload

# MOVE
def cmd_move(request, neptun, home_path):
    src_path = home_path+'/'+request.json['SOURCE']
    dst_path = home_path+'/'+request.json['DESTINATION']
    src_path = os.path.realpath(src_path)
    dst_path = os.path.realpath(dst_path)
    if not src_path.startswith(home_path):
        abort(400, 'Invalid source path.')
    if not dst_path.startswith(home_path):
        abort(400, 'Invalid destination path.')
    if os.path.exists(src_path) == True and os.path.exists(dst_path) == True and os.path.isdir(dst_path) == True:
        shutil.move(src_path, dst_path)
        return
    else:
    # TODO
        abort(400, "Can not move the file.")
COMMANDS['MOVE'] = cmd_move

# RENAME
def cmd_rename(request, neptun, home_path):
    src_path = home_path+'/'+request.json['PATH']
    src_path = os.path.realpath(src_path)
    if not src_path.startswith(home_path):
        abort(400, 'Invalid source path.')
    dst_path = os.path.dirname(src_path)+'/'+request.json['NEW_NAME']
    if os.path.exists(src_path) == True:
        os.rename(src_path, dst_path)
    else:
        abort(404, "File or Folder not found!")
COMMANDS['RENAME'] = cmd_rename

# NEW FOLDER
162
def cmd_new_folder(request, username, home_path):
163 164 165 166 167 168 169 170
    dir_path = home_path+'/'+request.json['PATH']
    dir_path = os.path.realpath(dir_path)
    if not dir_path.startswith(home_path):
        abort(400, 'Invalid directory path.')
    if os.path.exists(dir_path) == True:
        abort(400, "Directory already exist!")
    else:
        os.mkdir(dir_path, 0755)
171
        os.chown(dir_path, getpwnam(username).pw_uid, getpwnam(username).pw_gid)
172 173 174 175 176 177 178 179 180 181 182 183 184
COMMANDS['NEW_FOLDER'] = cmd_new_folder

# REMOVE
def cmd_remove(request, neptun, home_path):
    remove_path = home_path+'/'+request.json['PATH']
    remove_path = os.path.realpath(remove_path)
    if not remove_path.startswith(home_path):
        abort(400, 'Invalid path.')
    if os.path.exists(remove_path) != True:
        abort(404, "Path not found!")
    else:
        if os.path.isdir(remove_path) == True:
            shutil.rmtree(remove_path)
185 186
            return
        else:
187 188 189
            os.remove(remove_path)
            return
COMMANDS['REMOVE'] = cmd_remove
190

191 192 193 194 195 196 197 198 199 200 201
def cmd_toplist(request, neptun, home_path):
    d = []
    try:
        top_dir = os.path.normpath(os.path.join(home_path, "../.top"))
        d = [file_dict(os.readlink(os.path.join(top_dir, f)), home_path)
                for f in os.listdir(top_dir)]
    except:
        pass
    return json.dumps(sorted(d, key=lambda f: f['MTIME']))
COMMANDS['TOPLIST'] = cmd_toplist

202
@route('/set/<neptun>', method='POST')
203
@force_ssl
204 205 206 207 208 209 210 211 212
def set_keys(neptun):
    key_list = []
    smb_password = ''
    try:
        smbpasswd = request.json['SMBPASSWD']
        for key in request.json['KEYS']:
            key_list.append(key)
    except:
        abort(400, 'Wrong syntax!')
213
    result = subprocess.call([ROOT_BIN_FOLDER+'/'+USER_MANAGER, 'set', neptun, smbpasswd])
214
    if result == 0:
215
        updateSSHAuthorizedKeys(neptun, key_list)
216 217 218
        return
    elif result == 2:
        abort(403, 'User does not exist!')
219 220 221 222 223 224 225
@route('/quota/<neptun>', method='POST')
@force_ssl
def set_quota(neptun):
    try:
        quota = request.json['QUOTA']
    except:
        abort(400, 'Wrong syntax!')
tarokkk committed
226
    result = subprocess.call([ROOT_BIN_FOLDER+'/'+USER_MANAGER, 'setquota', neptun, str(quota), hard_quota(quota)])
227 228 229 230
    if result == 0:
        return
    elif result == 2:
        abort(403, 'User does not exist!')
231 232 233


@route('/new/<neptun>', method='POST')
234
@force_ssl
235 236
def new_user(neptun):
    key_list = []
tarokkk committed
237 238
    smbpasswd = ''
    quota = ''
239
    try:
240
        smbpasswd = request.json['SMBPASSWD']
tarokkk committed
241
        quota = request.json['QUOTA']
242
    except:
tarokkk committed
243
        print "Invalid syntax"
244
        abort(400, 'Invalid syntax')
245
    # Call user creator script
tarokkk committed
246
    result = subprocess.call([ROOT_BIN_FOLDER+'/'+USER_MANAGER, 'add', neptun, smbpasswd, str(quota), hard_quota(quota)])
247
    print "add "+neptun+" "+smbpasswd+" "+str(quota)+" "+hard_quota(quota)
248 249 250 251
    if result == 0:
        try:
            for key in request.json['KEYS']:
                key_list.append(key)
252
            updateSSHAuthorizedKeys(neptun, key_list)
253
        except:
tarokkk committed
254
            print "SSH error"
255
            abort(400, 'SSH')
256 257 258 259
        return
    elif result == 2:
        abort(403, 'User already exist!')
    else:
tarokkk committed
260
        print "Error"
261 262 263
        abort(400, 'An error occured!')


264 265

# Static file
266 267
@route('/dl/<hash_num>', method='GET')
def dl_hash(hash_num):
268
    hash_path = ROOT_WWW_FOLDER
269
    if os.path.exists(hash_path+'/'+hash_num) != True:
270 271 272
        abort(404, "File not found!")
    else:
        filename = os.path.basename(os.path.realpath(hash_path+'/'+hash_num))
273
        return static_file(hash_num, root=hash_path, download=filename)
274 275 276 277 278 279 280 281

@route('/ul/<hash_num>', method='OPTIONS')
def upload_allow(hash_num):
    response.set_header('Access-Control-Allow-Origin', '*')
    response.set_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
    response.set_header('Access-Control-Allow-Headers', 'Content-Type, Content-Range, Content-Disposition, Content-Description')
    return 'ok'

282 283 284 285 286 287 288 289 290 291 292 293 294 295

def _upload_save(uid, gid, input, path):
    os.setegid(gid)
    os.seteuid(uid)
    try:
        with open(path, 'wb', 0600) as output:
            while True:
                chunk = input.read(256*1024)
                if not chunk:
                    break
                output.write(chunk)
    finally:
        input.close()

296
@route('/ul/<hash_num>', method='POST')
297
def upload(hash_num):
298 299
    link = ROOT_WWW_FOLDER+'/'+hash_num
    if not os.path.exists(link):
300
        abort (404, 'Token not found!')
301 302 303 304
    try:
        file_data = request.files.data
        file_name = file_data.filename
    except:
305 306
        if os.path.exists(link):
            os.remove(link)
307
        abort(400, 'No file was specified!')
308
    up_path = os.path.realpath(link + '/' + file_name)
309 310
    if os.path.exists(up_path):
        abort(400, 'File already exists')
311
    if not up_path.startswith('/home'):
312
        abort(400, 'Invalid path.')
313 314
    linkstat = os.stat(link)
    os.remove(link)
315 316
    p = multiprocessing.Process(target=_upload_save,
            args=(linkstat.st_uid, linkstat.st_gid, file_data.file, up_path, ))
317
    try:
318 319
        p.start()
        p.join()
320
    finally:
321 322 323
        p.terminate()
    if p.exitcode:
        abort(400, 'Write failed.')
324
    try:
325 326
        redirect_address = request.headers.get('Referer')
    except:
327
        redirect_address = REDIRECT_URL
328 329 330
    response.set_header('Access-Control-Allow-Origin', '*')
    response.set_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
    response.set_header('Access-Control-Allow-Headers', 'Content-Type, Content-Range, Content-Disposition, Content-Description')
331
    redirect(redirect_address)
332

333 334
# Return hard quota from quota
def hard_quota(quota):
335
    return str(int(int(quota)*1.25))
336

337 338
# Update users .ssh/authorized_keys
def updateSSHAuthorizedKeys(username, key_list):
339 340
    user_uid=getpwnam(username).pw_uid
    user_gid=getpwnam(username).pw_gid
341
    auth_file_name = "/home/"+username+"/authorized_keys"
342 343 344
    with open(auth_file_name, 'w') as auth_file:
        for key in key_list:
            auth_file.write(key+'\n')
345 346
    os.chmod(auth_file_name, 0600)
    os.chown(auth_file_name, user_uid, user_gid)
347 348
    return

349 350 351
# For debug purpose
# @route('/ul/<hash_num>', method='GET')
# def upload_get(hash_num):
352 353
#    return """<form method="POST" action="/ul/{hash}" enctype="multipart/form-data">
#   <input name="data" type="file" />
354 355
#   <input type="submit" />
# </form>""".format(hash=hash_num)
356

357 358
def list_directory(home, path):
    # Check for path breakout
359
    if not os.path.realpath(path).startswith(home):
360
        abort(400, 'Invalid path.')
361
    # Check if path exist
362
    if os.path.exists(path) != True:
363
        abort(404, 'No such file or directory')
364
    else:
365
        # If it's a file return with list
366 367
        if os.path.isdir(path) != True:
            return json.dumps((os.path.basename(path), 'F', os.path.getsize(path), os.path.getmtime(path)))
368
        # List directory and return list
369 370
        else:
            filelist = os.listdir(path)
371 372 373 374 375 376 377 378 379 380 381
            dictlist = [file_dict(os.path.join(path, f), home) for f in filelist]
            return json.dumps(dictlist)

def file_dict(path, home):
    basename = os.path.basename(path.rstrip('/'))
    if os.path.isdir(path):
        is_dir = 'D'
    else:
        is_dir = 'F'
    return {'NAME': basename,
            'TYPE': is_dir,
382
            'SIZE': os.path.getsize(path),
383 384
            'MTIME': os.path.getmtime(path),
            'DIR': os.path.relpath(os.path.dirname(path), home)}
385

386
def get_quota(neptun):
387
    output=subprocess.check_output([ROOT_BIN_FOLDER+'/'+USER_MANAGER, 'status', neptun], stderr=subprocess.STDOUT)
388
    return output.split()
389

390 391 392 393 394 395 396
def set_quota(neptun, quota):
    try:
        output=subprocess.check_output([ROOT_BIN_FOLDER+'/'+USER_MANAGER, 'setquota', neptun, quota, hard_quota(quota)], stderr=subprocess.STDOUT)
    except:
        return False
    return True

397
if __name__ == "__main__":
398
    run(host=SITE_HOST, port=SITE_PORT)
399 400
else:
    application=app()