123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110 |
- #!/usr/bin/env python3
- from bottle import route, run, template, Bottle, request, response, static_file, HTTPError, abort, redirect
- import dataset # for database management
- from hashlib import md5, sha512 # for authentication
- import os # for file paths
- import time # for Expires and Last-Modified
- import magic # for those pesky mimetypes!
- import string
- from argparse import ArgumentParser # for the command line arguments
- from uuid import uuid4 # for pseudorandom number generation
- from random import SystemRandom # for file IDs and captcha generation
- from threading import Thread # for file deletion
- from sys import path, getsizeof
- path.insert(1, 'lib/')
- import safeurl
- from bottle_custom import custom_static_file
- from skimpyGimpy import skimpyAPI
- ROOT = '/var/www/p/'
- IS_DEV = False
- HOST = '127.0.0.1'
- PORT = 9090
- ALLOWED_CHARS = string.ascii_letters + string.digits + '_.-'
- CLEAN_INTERVAL = 300 # 5 minutes
- if __name__ == '__main__':
- parser = ArgumentParser(description='Hydra Paste, the little thing of ours.')
- parser.add_argument('--dev', help='developer mode; path', action='store_true')
- parser.add_argument('--host', help='host to listen at; ip or host', nargs='?', const=HOST)
- parser.add_argument('--port', help='port to listen at; integer 1024-65535', nargs='?', const=PORT, type=int)
- args = parser.parse_args()
- if args.dev == True:
- IS_DEV = True
- ROOT = os.getcwd()
- print('\nRunning in developer mode at `{}`'.format(ROOT))
- if args.host:
- HOST = args.host
- if args.port:
- PORT = args.port
- DIR_TEMPLATES = os.path.join(ROOT, 'static/')
- DIR_FILES = os.path.join(ROOT, 'files/')
- DATABASE = os.path.join(ROOT, 'db.sqlite')
- ui = Bottle()
- try:
- print('\nOpening database `{}`...'.format(DATABASE))
- db = dataset.connect('sqlite:///{}'.format(DATABASE))
- except:
- print('Unable to open database. Exiting program...')
- quit()
- pass
- SECRAND = SystemRandom() # generator of randomness
- CAPTCHA_SESSIONS = [] # captcha cookies
- #######################################
- # #
- # DON'T TOUCH SHIT FROM DOWN HERE #
- # #
- #######################################
- # Create the database tables if they don't exist
- for table_type in ['files', 'users', 'captcha', 'session']:
- if table_type not in db.tables:
- db.create_table(table_type)
- #######################################
- # Login, sessions and shit #
- #######################################
- def get_login_creds():
- """
- Get the username/password combination from a request
- Returns a tuple in the form of (username, password)
- """
- # TODO: Improve robustness of this code
- if request.method == 'POST':
- username = request.forms.getunicode('username')
- password = request.forms.getunicode('password')
- if not username:
- abort(401, 'You need to be logged in to do this')
- return (username, password)
- else:
- if 'Authorization' in request.headers:
- _method, user_pass_combo = request.headers['Authorization'].split()
- username, password = user_pass_combo.split(':')
- return (username, password)
- else:
- abort(401)
- def check_login(usr, pwd, anon=True):
- """
- Check for password login, or anonymous login.
- `usr` - username for login to check
- `pwd` - password for login to check
- `anon` - whether or not to allow anonymous logins
- Returns: `True` if login is good, `False` if otherwise.
- """
- if usr == 'anonymous': # Anonymous login
- return anon
- else: # TODO: Check SQLite DB
- result = db['users'].find_one(username=usr)
- if result and pwd: # login exists
- salt_hash_comb = result['p_hash']
- # cipher_num is for future migrations/logins
- # 6 = sha-512
- cipher_num, salt, login_hash = salt_hash_comb.split('$')
- calc_hash = sha512(bytes(salt + pwd, 'utf-8')).hexdigest()
- if calc_hash == login_hash:
- return True
- return False
- def check_login_session():
- """
- Check that a user's session is good by looking at their cookies.
- Cookies are generated in start_login_session()
- Cookies are removed in end_login_session()
- """
- login_session = request.get_cookie('login_session')
- if login_session and ':' in login_session:
- username, session = [x for x in login_session.split(':', 1)]
- user_row = db['session'].find_one(user=username, session=session)
- if user_row:
- match_id = '{}:{}'.format(user_row['user'], user_row['session'])
- return match_id == login_session
- else:
- return False
- else:
- return False
- def remove_session(session_id=None, username=None):
- """
- Search for a login session in the sqlite database and remove it
- if the user or session exists.
- For use with internal API only.
- `session_id` - Session ID that will be removed
- `username` - If `session_id` is not specified, `username` will get session removed
- """
- assert not (session_id and username), 'You may only pass one parameter at once'
- assert username or session_id, 'You must pass a parameter to this function'
- results = None
- if session_id:
- results = db['session'].find(session=session_id)
- elif username:
- results = db['session'].find(user=username)
- else:
- abort(500, 'Uh oh. Were you not logged in?')
- db.begin()
- for row in results:
- row_id = row['id']
- db['session'].delete(id=row_id)
- print('Deleted row {}'.format(row_id))
- db.commit()
- def start_login_session(usr, remember=False):
- """
- Gives a user a cookie to start their login session
- `usr` - The user that will get a cookie
- `remember` - Whether to have the login session "remembered"
- """
- # Find session for user in database and remove it
- remove_session(username=usr)
- # Format of the session cookie is `user:session_id`
- session_id = sha512(bytes(uuid4().hex, 'utf-8')).hexdigest()
- session_cookie = '{}:{}'.format(usr, session_id)
- # Add session to database
- db.begin()
- db['session'].insert({'user': usr, 'session': session_id})
- db.commit()
- if remember:
- expire_time = time.time() + 14 * 24 * 3600 # Expire in 14 days
- response.set_cookie('login_session', session_cookie, expires=expire_time)
- else:
- response.set_cookie('login_session', session_cookie)
- def _get_session_username():
- """
- Get the username of the current session in the request
- NOTE: This DOES NOT verify the user is logged in, and should
- be used for cosmetic purposes only.
- Returns: `username` of the user with a cookie (?)
- """
- login_session = request.get_cookie('login_session')
- if login_session and ':' in login_session:
- return login_session.split(':', 1)[0]
- else:
- return None
- def check_captcha(cookie_id, captcha_answer):
- """
- Check a captcha answer and cookie ID against stored credentials
- `cookie_id` - Unique ID of a session
- `captcha_answer` - Answer returned by the user
- Returns: `True` if the answer matches the known solution
- """
- for auth_pair in CAPTCHA_SESSIONS:
- print(auth_pair)
- if auth_pair[0] == cookie_id and auth_pair[1] == captcha_answer:
- CAPTCHA_SESSIONS.remove(auth_pair) # Remove correctly solved session, we don't need it anymore
- return True
- return False
- #######################################
- # General functions #
- #######################################
- def generate_etag(param_list):
- """
- Generate a weak ETag from a list of parameters.
- ETAG is generated from concatenating items in param_list,
- and hashing the result through an MD5 Hash.
- MD5 is used because it is fast. In this case, the ETag does
- not have to be secure, so using MD5 is not an issue.
- `param_list` - List of things to be hashed into an ETag
- Returns: ETag of `param_list`
- """
- combo = ''
- for param in param_list:
- combo += str(param)
- # 'W/' for weak ETags
- return 'W/' + md5(bytes(combo, 'utf-8')).hexdigest()
- def get_random_key():
- """
- Get a file key that isn't already taken
- Returns: an unique key
- """
- key = SECRAND.randint(0, 66 ** 4)
- while True:
- if not db['files'].find_one(key=key):
- return key
- key = SECRAND.randint(0, 66 ** 4) # O(1) operation
- def sanitize(unsafe_input):
- """
- Check for unsafe input
- """
- for character in unsafe_input:
- if character not in ALLOWED_CHARS:
- return False
- return True
- def user_pastes(username):
- """
- Return a dicitonary of a user's pastes
- username - the user to get pastes from
- """
- search_results = db['files'].find(user=username)
- data = {}
- for paste in search_results:
- key = paste['key']
- short_id = safeurl.num_encode(key)
- data[short_id] = api_file_info(short_id)
- return data
- #######################################
- # Files and shit #
- #######################################
- def get_file(fileid, mime_type, req=None, download=False, filename=None):
- """
- Get a file
- `fileid` - The unique ID of a file
- `mime_type` - Used to identify a file's type
- `headers` - If we want to specify special headers
- `req` - Internal request object
- `download` - Force download of a file
- `filename` - Original name of a file
- Returns: File
- """
- file_info = api_file_info(fileid, headers=True)
- if download:
- return custom_static_file(fileid, root=DIR_FILES, request=req, custom_headers=file_info, mimetype=mime_type, download=filename)
- else:
- return custom_static_file(fileid, root=DIR_FILES, request=req, custom_headers=file_info, mimetype=mime_type)
- def increment_view_count(paste):
- """
- Increment the view counter of a paste by 1.
- `paste` - the short key of a paste
- """
- long_key = safeurl.num_decode(paste)
- row = db['files'].find_one(key=long_key)
- db.begin()
- db['files'].update({'key': row['key'], 'views': (row['views'] + 1)}, ['key'])
- db.commit()
- #######################################
- # API Stuff #
- #######################################
- @ui.post('/api/user')
- @ui.post('/api/user/')
- def api_user_pastes():
- """
- API for viewing a user's pastes.
- User must be logged in to see own pastes.
- Returns: Data if the user is logged in, otherwise error 401
- """
- username, password = get_login_creds()
- if check_login(username, password, anon=False):
- return user_pastes(username)
- elif username == 'anonymous':
- abort(401, 'You must have an account to do this')
- else:
- abort(401, 'You need to log in to do this.')
- @ui.delete('/api/file/<fileid>') # REST API
- @ui.post('/api/file/rm/<fileid>') # Pleb API
- def api_file_delete(fileid):
- """
- Delete a file by its file id.
- Authorization is in the user-pass format.
- """
- user, password = get_login_creds()
- file_info = api_file_info(fileid)
- long_key = safeurl.num_decode(fileid)
- if check_login(user, password, anon=False) and file_info['user'].lower() == user.lower():
- long_key = safeurl.num_decode(fileid)
- db.begin()
- db['files'].delete(key=long_key)
- db.commit()
- try:
- os.remove(os.path.join(DIR_FILES, fileid))
- except:
- abort(500, 'Something bad happened. That file isn\'t here :(')
- print('[DBG] Deleted file: {}'.format(fileid))
- return 'File deleted'
- else:
- abort(401)
- @ui.route('/api/file/<fileid>')
- @ui.route('/api/file/get/<fileid>') # TODO: Depreciate
- def api_get_file(fileid):
- """
- Retrieve information about a file or download it.
- #: `GET` - Downloads file `fileid`
- #: `HEAD` - Downloads information about a file from it's headers.
- """
- real_key = safeurl.num_decode(fileid)
- row = db['files'].find_one(key=real_key) # At this point, `fileid` is safe
- if row:
- dupe_view = False # Whether this view is a duplicate view
- # Check if referer is web UI
- if 'Referer' in request.headers:
- referer = request.headers['Referer']
- if '.' in referer:
- base_referer = referer.split('.', 1)
- if base_referer[1] == 'hydra.ws/p/{}'.format(fileid):
- dupe_view = True
- if not dupe_view and request.method == 'GET':
- increment_view_count(fileid)
- mime_type = row['filetype']
- return get_file(fileid, mime_type, req=request)
- else:
- abort(404)
- @ui.route('/api/file/info/<fileid>')
- def api_file_info(fileid, headers=False):
- """
- Get information about a file
- `fileid` - Unique file ID
- `headers` - To send custom headers or not
- Returns: Information about `fileid`, error 404 otherwise
- """
- file_key = safeurl.num_decode(fileid)
- db_result = db['files'].find_one(key=file_key)
- if db_result and headers:
- # Create ETag for file
- last_modified = db_result['time']
- filename = db_result['filename']
- etag = generate_etag(('static-', filename, last_modified))
- return {'X-Paste-User': db_result['user'],
- 'X-Paste-Expires': db_result['expires'],
- 'X-Paste-Filename': filename,
- 'X-Paste-Title': db_result['title'],
- 'X-Paste-Views': db_result['views'],
- 'Content-Type': db_result['filetype'],
- 'Content-Length': db_result['filesize'],
- 'Last-Modified': time.strftime('%a, %d %b %Y %H:%M:%S +0000', time.gmtime(last_modified)),
- 'ETag': etag}
- elif db_result and not headers:
- return {'user': db_result['user'],
- 'expires': int(db_result['expires']),
- 'filename': db_result['filename'],
- 'filetype': db_result['filetype'],
- 'filesize': int(db_result['filesize']),
- 'title': db_result['title'],
- 'time': db_result['time'],
- 'views': db_result['views'],
- 'key': fileid}
- else:
- abort(404)
- @ui.route('/api/upload')
- def wrong_get_upload():
- abort(400, 'Use a POST, not a GET.')
- @ui.post('/api/file/<fileid>') # For file updates
- @ui.post('/api/upload') # For initial uploads
- def api_upload(fileid=None, web_ui=False, username=None):
- """
- Function for uploading files to server.
- Allows option to modify existing file if uploaded from personal account.
- fileid - if non-blank, the short ID of the file to upload
- web_ui - whether this upload was via the web ui - assumes correct authentication
- username - webui username
- """
- password = None
- if not web_ui:
- username, password = get_login_creds()
- file_text = request.forms.getunicode('text', default=None)
- if username.strip() == '':
- abort(401, 'You must specify a login to use this command')
- if not web_ui and not check_login(username, password, anon=True):
- abort(401, 'Bad login')
- # File upload information
- upload_time = int(time.time())
- upload = request.files.get('upload')
- upload_size = None
- file_name = None
- if upload:
- upload_size = request.content_length # For internal purposes, this
- # is a rough hack because bottle's FileUpload.content_length is broken
- file_name = upload.filename
- else:
- upload_size = getsizeof(bytes(file_text, 'utf-8'))
- # Size + 400 because of metadata included in request header
- if file_text and upload_size >= 10000000 + 400:
- abort(413, 'You may only upload text up to 10 MB large')
- elif not file_text and upload_size >= 150000000 + 400:
- abort(413, 'You may only upload files up to 150 MB large')
- expires = None
- try:
- expires = request.forms.get('expires')
- delta_time = None
- if '+' in expires: # If delta time
- delta_time = int(expires.strip()[1:])
- expires = int(time.time()) + delta_time
- else: # If manual time time
- if expires is None: # not specified, give default time
- expires = int(time.time()) + 518400 # 518400 sec = 6 days
- elif int(expires) == 0: # Never expires
- expires = 0
- else: # Give custom time
- expires = int(expires)
- except:
- abort(400, 'The "expires" parameter was messed up. Please refer to the API for correct usage.')
- # Paste title
- paste_title = request.forms.getunicode('title')
- if paste_title.strip() == '':
- paste_title = file_name if file_name else 'Untitled'
- if fileid: # If updating an existing file
- file_info = api_file_info(fileid)
- if file_info['user'] == 'anonymous': # If upload is anonymous
- abort(401, 'You cannot edit anonymous uploads!')
- elif username == file_info['user']: # If file belongs to user
- file_data = upload.file # TODO So we can also check for 'textfield' variable
- save_path = os.path.join(DIR_FILES, fileid)
- with open(save_path, 'wb') as f:
- f.write(file_data.read())
- filesize = os.stat(save_path).st_size
- file_info['time'] = int(time.time())
- file_info['filesize'] = int(filesize)
- file_info['title'] = paste_title
- file_info['expires'] = int(expires)
- file_info['key'] = safeurl.num_decode(fileid)
- #print(file_info)
- db.begin()
- db['files'].update(file_info, ['key'])
- db.commit()
- return 'Success! File Modified: {}'.format(fileid)
- else: # only other possibility is that the user does not own the file!
- abort(401, """You don't have permission to edit that file!\n"""
- """It belongs to "{}"!""".format(file_info['user']))
- key = get_random_key()
- short_key = safeurl.num_encode(key)
- # Add file to database
- db.begin()
- db['files'].insert({'user': username,
- 'expires': int(expires), # If 0, never expires. If defined, expires @ epoch time
- 'filename': file_name,
- 'filetype': None,
- 'filesize': int(0),
- 'time': upload_time,
- 'views': 0,
- 'key': key,
- 'title': paste_title})
- save_path = os.path.join(DIR_FILES, short_key)
- with open(save_path, 'wb') as save_file:
- if upload: # If we're using a file
- save_file.write(upload.file.read())
- else:
- save_file.write(bytes(file_text, 'utf-8'))
- # Detect Mime or get paste
- mime_type = request.forms.getunicode('mimetype')
- if not mime_type or mime_type == 'auto':
- mime_type = magic.from_file(save_path, mime=True).decode('utf-8')
- row_id = db['files'].find_one(key=key)['id']
- filesize = os.stat(save_path).st_size
- #print('Editing MIME type to {}'.format(mime_type))
- db['files'].update({'id': row_id, 'filetype': mime_type, 'filesize': filesize}, ['id'])
- db.commit()
- return 'Success! File ID: {}'.format(short_key)
- #######################################
- # Web UI #
- #######################################
- @ui.route('/user')
- @ui.route('/user/')
- def ui_user_pastes():
- """
- Page to show a user their pastes.
- """
- if not check_login_session():
- redirect('/login?redirect=/user')
- #abort(401, 'Authorization required for this page, please login.')
- username = _get_session_username()
- pastes = user_pastes(username)
- return gen_page(os.path.join(DIR_TEMPLATES, 'userpastes.tpl'),
- {'pastes': pastes,
- 'username': username})
- @ui.route('/user/changepass')
- @ui.post('/user/changepass')
- def user_change_password():
- """
- Change a users's password
- """
- if not check_login_session():
- redirect('/login')
- data = {'error': None,
- 'success': None}
- username = _get_session_username()
- data['username'] = username
- if request.method == 'POST':
- current = request.forms.getunicode('pass-current')
- pass1 = request.forms.getunicode('pass-new')
- pass2 = request.forms.getunicode('pass-confirm')
- if not check_login(username, current, anon=False):
- data['error'] = 'Incorrect current password. Please try again.'
- elif pass1 != pass2:
- data['error'] = 'Passwords don\'t match. Please try again.'
- elif pass1 == current:
- data['error'] = 'Current and new password match'
- elif len(pass1) <= 6:
- data['error'] = 'Password too weak. Try using more than 6 characters.'
- else:
- salt = uuid4().hex
- pass_hash = sha512(bytes(salt + pass1, 'utf-8')).hexdigest()
- hash_salt_comb = '$6$' + salt + '$' + pass_hash
- db.begin()
- db['users'].update({'username': username, 'p_hash': hash_salt_comb}, ('username'))
- db.commit()
- data['success'] = True
- return gen_page(os.path.join(DIR_TEMPLATES, 'changepass.tpl'), data)
- @ui.route('/delete/<fileid>')
- @ui.post('/delete/<fileid>')
- def ui_delete_file(fileid):
- """
- Webpage to allow users to delte files
- """
- if not check_login_session():
- redirect('/login?redirect=/delete/{}'.format(fileid))
- #abort(401, 'Authorization required for this page, please login.')
- username = _get_session_username()
- data = {'deleted': False,
- 'warning': False,
- 'fileid': fileid}
- file_info = api_file_info(fileid)
- data['pastetitle'] = file_info['title']
- if username != file_info['user']:
- abort(401, 'You aren\'t allowed to delete this file!')
- if request.method == 'POST':
- delete = request.forms.getunicode('delete')
- print('delete {}'.format(delete))
- if delete:
- data['deleted'] = True
- long_key = safeurl.num_decode(fileid)
- db.begin()
- db['files'].delete(key=long_key)
- db.commit()
- try:
- os.remove(os.path.join(DIR_FILES, fileid))
- except:
- abort(500, 'Something bad happened. That file isn\'t here :(')
- print('[DBG] Deleted file: {}'.format(fileid))
- else:
- redirect('/user')
- elif request.method == 'GET':
- data['warning'] = True
- else:
- pass
- return gen_page(os.path.join(DIR_TEMPLATES, 'deletefile.tpl'), data)
- @ui.route('/logout')
- def end_login_session():
- """
- End's a user's login session. Also removes their login cookie.
- """
- if not check_login_session():
- abort(400, 'You need to be logged in to do this!')
- # Get username of user, delete cookie, and remove session from database
- username = _get_session_username()
- remove_session(username=username)
- response.delete_cookie('login_session')
- redirect('/login')
- @ui.route('/uploadcombined') # temporary redirect for fixing links
- def upload_redirect():
- redirect('/upload')
- @ui.route('/upload')
- def web_upload_page():
- page_data = {}
- isFile = request.query.file
- if isFile:
- page_data['isFile'] = 'true'
- else:
- page_data['isFile'] = 'false'
- username = _get_session_username()
- if username:
- page_data['username'] = username
- return gen_page(os.path.join(DIR_TEMPLATES, 'upload.tpl'), page_data)
- @ui.post('/upload') # Combined upload page
- def web_upload_post():
- cookie_username = _get_session_username()
- if cookie_username: # User is logged in w/ cookies
- if check_login_session():
- output = api_upload(web_ui=True, username=cookie_username)
- if 'Success' in output:
- key = output.split(':')[1].strip()
- data = {'key': key,
- 'username': cookie_username}
- return gen_page(os.path.join(DIR_TEMPLATES, 'uploadsuccess.tpl'), data)
- else:
- output = api_upload()
- if 'Success' in output:
- key = output.split(':')[1].strip()
- data = {'key': key}
- return gen_page(os.path.join(DIR_TEMPLATES, 'uploadsuccess.tpl'), data)
- @ui.route('/register')
- @ui.post('/register') # Receiving registration data
- def ui_register():
- """
- Register a user with the web frontent ui
- """
- reg_data = {'success_text': None,
- 'error_text': None}
- if request.method == 'POST': # see if we need to verify registration
- username = request.forms.getunicode('username')
- password = request.forms.getunicode('password')
- password_confirm = request.forms.getunicode('password-confirm')
- captcha_answer = request.forms.getunicode('captcha-challenge')
- captcha_id = request.cookies.getunicode('captcha_id')
- email = request.forms.getunicode('email') or None
- captcha_result = check_captcha(captcha_id, captcha_answer)
- if not sanitize(username):
- reg_data['error_text'] = """Usernames can only contain the """ \
- """following characters: {}""".format(ALLOWED_CHARS)
- elif len(username) < 2 or len(username) > 32:
- reg_data['error_text'] = 'Your username\'s length must be between 2 and 32 characters inclusive.'
- elif password != password_confirm:
- reg_data['error_text'] = "Those passwords don't match. Please try again."
- elif not captcha_result:
- reg_data['error_text'] = "Sorry, but you failed the captcha, please try again."
- elif len(password) <= 6:
- reg_data['error_text'] = 'Password too weak. Please try using more than 6 characters.'
- elif len(password) > 128:
- reg_data['error_text'] = """Your password is really long! Try again with a password """\
- """that is 128 characters or fewer."""
- else:
- # Check if username already exists
- taken = False
- if len(db['users']) > 0:
- users = db['users'].table
- query = users.select(users.c.username.ilike('%' + username + '%'))
- result = db.query(query)
- # See if there are any results
- for row in result:
- taken = True
- break
- if taken:
- reg_data['error_text'] = 'Sorry, but the username {} is already taken.'.format(username)
- elif username.lower().strip() == 'anonymous':
- reg_data['error_text'] = 'You are legion. You cannot register with "anonymous".'
- elif username.lower().strip() == 'root':
- reg_data['error_text'] = 'You must be a superuser to do that ;)'
- elif email and not ('@' in email): # There are better ways to do this, but whatever
- reg_data['error_text'] = 'That email is invalid. Please try again.'
- else:
- joined = int(time.time())
- salt = uuid4().hex
- p_hash = sha512(bytes(salt + password, 'utf-8')).hexdigest()
- # TODO: Use correct hash naming scheme with 3$salt$password
- hash_salt_comb = '$' + salt + '$' + p_hash
- db.begin()
- db['users'].insert({'username': username,
- 'joined': joined,
- 'pastes': 0,
- 'p_hash': hash_salt_comb,
- 'email': email})
- db.commit()
- print('Registered user: {}'.format(username))
- reg_data['success_text'] = username
- # Log user in and stuff
- start_login_session(username)
- reg_data['username'] = username
- reg_data['captcha_data'] = captcha_provider()
- return gen_page(os.path.join(DIR_TEMPLATES, 'register.tpl'), reg_data)
- def captcha_provider():
- """
- Generate random CAPTCHA using skimpyGimpy, give use CAPTCHA ID Cookie, and
- store CAPTCHA credential in database.
- """
- # Generate a random string 4 characters long
- pass_phrase = ''.join(SECRAND.choice(string.ascii_uppercase + string.digits) for _ in range(4))
- # Yes; use MD5 because the cookie doesn't have to be cryptographically secure.
- captcha_cookie_id = md5(bytes(str(uuid4()), 'utf-8')).hexdigest()
- # If there are already 10 sessions for creating cookies, expire the first session
- if len(CAPTCHA_SESSIONS) >= 10:
- _ = CAPTCHA_SESSIONS.pop(0)
- CAPTCHA_SESSIONS.append((captcha_cookie_id, pass_phrase))
- # Generate CAPTCHA with skimpyGimpy
- captcha_gen = skimpyAPI.Pre(pass_phrase, speckle=0.33, scale=1.33, color='#fff')
- captcha_test = captcha_gen.data()
- response.set_cookie('captcha_id', captcha_cookie_id)
- #print('Cookie {} coresponds to solution {}'.format(captcha_cookie_id, pass_phrase))
- return captcha_test
- @ui.route('/login')
- @ui.post('/login')
- def ui_login():
- """
- Login for UI
- """
- login_data = {'success': False,
- 'failure': False,
- 'anon_failure': False,
- 'login_needed': False,
- 'redirect': None}
- redirect_url = request.query.redirect
- if request.method == 'POST': # see if we need to verify registration
- username, password = get_login_creds()
- if check_login(username, password, anon=False):
- login_data['success'] = username
- if request.forms.getunicode('remember'):
- start_login_session(username, remember=True)
- else:
- start_login_session(username)
- if request.forms.getunicode('redirect'):
- redirect(request.forms.getunicode('redirect'))
- elif username.lower() == 'anonymous':
- login_data['anon_failure'] = True
- elif redirect_url:
- login_data['login_needed'] = True
- login_data['redirect'] = request.query.redirect
- else:
- login_data['failure'] = True
- else:
- if redirect_url:
- login_data['login_needed'] = True
- login_data['redirect'] = redirect_url
- return gen_page(os.path.join(DIR_TEMPLATES, 'login.tpl'), login_data)
- @ui.route('/p/<paste>')
- def get_ui_paste(paste):
- # Get file_data first to handle 404s
- file_data = api_file_info(paste)
- if request.method == 'GET': # Fix for HEAD to inflate view count
- increment_view_count(paste)
- file_data['short_key'] = paste
- file_data['file_internal_src'] = os.path.join(DIR_FILES, paste)
- file_data['file_external_src'] = '/api/file/{}'.format(paste)
- if not file_data['title']:
- file_data['title'] = 'Untitled Paste'
- return gen_page(os.path.join(DIR_TEMPLATES, 'paste.tpl'), file_data)
- @ui.error(404)
- def error404(error_text):
- return '404 - Sorry man, but the file isn\'t here :('
- def gen_page(template_path, data={}):
- """
- Generates a page with the headers and footers, along with the data dictionary given
- template_path - abosolute path of the template to use
- data - any page-specific data that needs to be given to the page
- Optional Keys:
- username - username of logged in user
- """
- data['top'] = os.path.join(DIR_TEMPLATES, 'top.html')
- data['bottom'] = os.path.join(DIR_TEMPLATES, 'bottom.html')
- data['footer'] = os.path.join(DIR_TEMPLATES, 'footer.html')
- data['stylesheets'] = tuple()
- data['scripts'] = tuple()
- data['og_image'] = '/static/favicon.ico'
- data['card_type'] = 'summary'
- if 'username' not in data:
- username = _get_session_username()
- if username:
- data['username'] = username
- else:
- data['username'] = None
- # Standard date time
- if 'time' in data:
- mod_time = data['time']
- time_diff = int(time.time()) - mod_time
- suffix = None
- time_unit = 0
- # Fast natural date implementation
- if time_diff <= 1 or time_diff <= 60:
- time_unit = time_diff # Diff in seconds
- if time_diff == 1:
- suffix = 'second'
- else:
- suffix = 'seconds'
- elif time_diff > 60 and time_diff < 3600:
- # 60 secs/min
- time_unit = time_diff // 60
- if time_diff < 120:
- suffix = 'minute'
- else:
- suffix = 'minutes'
- elif time_diff >= 3600 and time_diff < 86400:
- # 60^2 sec/hr
- time_unit = time_diff // (60 * 60)
- if time_diff < 7200:
- suffix = 'hour'
- else:
- suffix = 'hours'
- else:
- # 60^2 * 24 sec/day
- time_unit = time_diff // (60 * 60 * 24)
- suffix = 'days'
- data['time'] = '{} {} ago'.format(time_unit, suffix)
- return template(template_path, data)
- @ui.route('/') # Index Page
- @ui.route('/<filename>') # Favicon.ico workaround
- @ui.route('/static/<filename:path>') # All other static files
- def static_route(filename=None):
- """
- Route files in DIR_TEMPLATES when using --dev mode.
- Route homepage whenever using production setup.
- Also routes the index file (found in `/`)
- """
- print('Local Routing with {}'.format(filename))
- if not filename: # Frontpage
- return gen_page(os.path.join(DIR_TEMPLATES, 'frontpage.tpl'))
- elif filename == 'favicon.ico': # Favicon workaround
- return static_file(filename, root=DIR_TEMPLATES)
- else:
- return static_file(filename, root=DIR_TEMPLATES)
- def prune_files(interval, _db):
- """
- Checks which files have expired given int(interval) as a second-format cooldown.
- """
- while True:
- time.sleep(interval)
- print('Cleaning shit up')
- now = int(time.time())
- try:
- # This is the fastest way to extract all files' information from the DB,
- # so the transaction is unlikely to get interrupted and crash!
- all_files = [fil for fil in _db['files'].all()]
- except:
- continue
- _db.begin()
- for _file in all_files:
- expires = int(_file['expires'])
- if expires <= now and expires:
- _db['files'].delete(id=_file['id'])
- _db.commit()
- key = safeurl.num_encode(_file['key'])
- try:
- os.remove(os.path.join(DIR_FILES, key))
- except:
- continue
- print("[DBG] Deleted file: %i\n\t%s" % (_file['id'], key))
- _db.commit()
- print('Shit cleaned up')
- if __name__ == '__main__':
- # check to make sure `files` folder exists, if not, then create it.
- if not os.path.exists(DIR_FILES):
- os.makedirs(DIR_FILES)
- # Starting the prune_files(interval) thread.
- t_prune = Thread(target=prune_files, args=(CLEAN_INTERVAL, db))
- t_prune.start()
- run(ui, host=HOST, port=PORT)
|