CloudStore.py 13.5 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 11
from pwd import getpwnam

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


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')
21
# Standalone server
22 23
SITE_HOST = config.get('store', 'site_host')
SITE_PORT = config.get('store', 'site_port')
24
# Temporary dir for tar.gz
25
TEMP_DIR = config.get('store', 'temp_dir')
26 27 28 29 30
#Redirect
try:
    REDIRECT_URL = config.get('store', 'redirect_url')
except:
    REDIRECT_URL = "https://cloud.ik.bme.hu"
31 32 33 34 35 36 37 38 39
#ForceSSL
try:
    FORCE_SSL = config.get('store', 'force_ssl') == "True"
except:
    FORCE_SSL = False


def force_ssl(original_function):
    def new_function(*args, **kwargs):
40 41 42 43 44 45
        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)
46 47
        else:
            return original_function(*args, **kwargs)
48
    return new_function
49 50

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

60
# @route('/<neptun:re:[a-zA-Z0-9]{6}>', method='GET')
61
@route('/<neptun>', method='GET')
62
@force_ssl
63 64 65 66 67
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:
68
        statistics=get_quota(neptun)
69 70
        return { 'Used' : statistics[0], 'Soft' : statistics[1], 'Hard' : statistics[2]}

71 72
COMMANDS = {}

73
@route('/<neptun>', method='POST')
74
@force_ssl
75
def neptun_POST(neptun):
76
    # Check if user avaiable (home folder ready)
77 78 79 80
    home_path = '/home/'+neptun+'/home'
    if os.path.exists(home_path) != True:
        abort(401, 'The requested user does not exist!')
    else:
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 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 162 163 164 165 166 167
        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())
    if( os.path.isfile(dl_path) ):
        os.symlink(dl_path, ROOT_WWW_FOLDER+'/'+dl_hash)
        # Debug
        # redirect('http://store.cloud.ik.bme.hu:8080/dl/'+dl_hash)
        return json.dumps({'LINK' : SITE_URL+'/dl/'+dl_hash})
    else:
        try:
            os.makedirs(TEMP_DIR+'/'+neptun, 0700)
        except:
            pass
        folder_name = os.path.basename(dl_path)
        temp_path = TEMP_DIR+'/'+neptun+'/'+folder_name+'.zip'
        with open(os.devnull, "w") as fnull:
            # zip -rqDj vmi.zip /home/tarokkk/vpn-ik
            result = subprocess.call(['/usr/bin/zip', '-rqDj', temp_path, dl_path], stdout = fnull, stderr = fnull)
        os.symlink(temp_path, ROOT_WWW_FOLDER+'/'+dl_hash)
        return json.dumps({'LINK' : SITE_URL+'/dl/'+dl_hash})
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())
        os.symlink(up_path, ROOT_WWW_FOLDER+'/'+up_hash)
        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
168
def cmd_new_folder(request, username, home_path):
169 170 171 172 173 174 175 176
    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)
177
        os.chown(dir_path, getpwnam(username).pw_uid, getpwnam(username).pw_gid)
178 179 180 181 182 183 184 185 186 187 188 189 190
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)
191 192
            return
        else:
193 194 195
            os.remove(remove_path)
            return
COMMANDS['REMOVE'] = cmd_remove
196

197 198 199 200 201 202 203 204 205 206 207
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

208
@route('/set/<neptun>', method='POST')
209
@force_ssl
210 211 212 213 214 215 216 217 218
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!')
219
    result = subprocess.call([ROOT_BIN_FOLDER+'/'+USER_MANAGER, 'set', neptun, smbpasswd])
220
    if result == 0:
221
        updateSSHAuthorizedKeys(neptun, key_list)
222 223 224
        return
    elif result == 2:
        abort(403, 'User does not exist!')
225 226 227 228 229 230 231
@route('/quota/<neptun>', method='POST')
@force_ssl
def set_quota(neptun):
    try:
        quota = request.json['QUOTA']
    except:
        abort(400, 'Wrong syntax!')
tarokkk committed
232
    result = subprocess.call([ROOT_BIN_FOLDER+'/'+USER_MANAGER, 'setquota', neptun, str(quota), hard_quota(quota)])
233 234 235 236
    if result == 0:
        return
    elif result == 2:
        abort(403, 'User does not exist!')
237 238 239


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


270 271

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

@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'

288
@route('/ul/<hash_num>', method='POST')
289 290
def upload(hash_num):
    if not os.path.exists(ROOT_WWW_FOLDER+'/'+hash_num):
291
        abort (404, 'Token not found!')
292 293 294 295 296 297 298 299 300 301
    try:
        file_data = request.files.data
        file_name = file_data.filename
    except:
        if os.path.exists(ROOT_WWW_FOLDER+'/'+hash_num):
            os.remove(ROOT_WWW_FOLDER+'/'+hash_num)
        abort(400, 'No file was specified!')
    up_path = os.path.realpath(ROOT_WWW_FOLDER+'/'+hash_num+'/'+file_name)
    if os.path.exists(up_path):
        abort(400, 'File already exists')
302
    # Check if upload path valid
303
    if not up_path.startswith('/home'):
304
        abort(400, 'Invalid path.')
305

306
    os.remove(ROOT_WWW_FOLDER+'/'+hash_num)
307 308 309 310 311 312 313 314
    # Get the real upload path
    # Delete the hash link
    # Get the username from path for proper ownership
    username=up_path.split('/', 3)[2]
    # os.setegid(getpwnam(username).pw_gid)
    # os.seteuid(getpwnam(username).pw_uid)
    # TODO setuid subcommand
    # Check if file exist (root can overwrite anything not safe)
315 316
    with open(up_path , 'wb') as f:
        datalength = 0
317
        for chunk in fbuffer(file_data.file, chunk_size=1048576):
318 319
            f.write(chunk)
            datalength += len(chunk)
320 321
    os.chown(up_path, getpwnam(username).pw_uid, getpwnam(username).pw_gid)
    os.chmod(up_path, 0644)
322
    try:
323 324
        redirect_address = request.headers.get('Referer')
    except:
325
	    redirect_address = REDIRECT_URL
326 327 328
    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')
329 330
    redirect(redirect_address)
    #return 'Upload finished: '+file_name+' - '+str(datalength)+' Byte'
331

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

336
# Define filebuffer for big uploads
337 338 339 340 341 342
def fbuffer(f, chunk_size=4096):
   while True:
      chunk = f.read(chunk_size)
      if not chunk: break
      yield chunk

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

355 356 357
# For debug purpose
# @route('/ul/<hash_num>', method='GET')
# def upload_get(hash_num):
358 359
#    return """<form method="POST" action="/ul/{hash}" enctype="multipart/form-data">
#   <input name="data" type="file" />
360 361
#   <input type="submit" />
# </form>""".format(hash=hash_num)
362

363 364
def list_directory(home, path):
    # Check for path breakout
365
    if not os.path.realpath(path).startswith(home):
366
        abort(400, 'Invalid path.')
367
    # Check if path exist
368
    if os.path.exists(path) != True:
369
        abort(404, 'No such file or directory')
370
    else:
371
        # If it's a file return with list
372 373
        if os.path.isdir(path) != True:
            return json.dumps((os.path.basename(path), 'F', os.path.getsize(path), os.path.getmtime(path)))
374
        # List directory and return list
375 376
        else:
            filelist = os.listdir(path)
377 378 379 380 381 382 383 384 385 386 387
            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,
388
            'SIZE': os.path.getsize(path),
389 390
            'MTIME': os.path.getmtime(path),
            'DIR': os.path.relpath(os.path.dirname(path), home)}
391

392
def get_quota(neptun):
393
    output=subprocess.check_output([ROOT_BIN_FOLDER+'/'+USER_MANAGER, 'status', neptun], stderr=subprocess.STDOUT)
394
    return output.split()
395

396 397 398 399 400 401 402
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

403
if __name__ == "__main__":
404
    run(host=SITE_HOST, port=SITE_PORT)
405 406
else:
    application=app()