hydrapaste.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110
  1. #!/usr/bin/env python3
  2. from bottle import route, run, template, Bottle, request, response, static_file, HTTPError, abort, redirect
  3. import dataset # for database management
  4. from hashlib import md5, sha512 # for authentication
  5. import os # for file paths
  6. import time # for Expires and Last-Modified
  7. import magic # for those pesky mimetypes!
  8. import string
  9. from argparse import ArgumentParser # for the command line arguments
  10. from uuid import uuid4 # for pseudorandom number generation
  11. from random import SystemRandom # for file IDs and captcha generation
  12. from threading import Thread # for file deletion
  13. from sys import path, getsizeof
  14. path.insert(1, 'lib/')
  15. import safeurl
  16. from bottle_custom import custom_static_file
  17. from skimpyGimpy import skimpyAPI
  18. ROOT = '/var/www/p/'
  19. IS_DEV = False
  20. HOST = '127.0.0.1'
  21. PORT = 9090
  22. ALLOWED_CHARS = string.ascii_letters + string.digits + '_.-'
  23. CLEAN_INTERVAL = 300 # 5 minutes
  24. if __name__ == '__main__':
  25. parser = ArgumentParser(description='Hydra Paste, the little thing of ours.')
  26. parser.add_argument('--dev', help='developer mode; path', action='store_true')
  27. parser.add_argument('--host', help='host to listen at; ip or host', nargs='?', const=HOST)
  28. parser.add_argument('--port', help='port to listen at; integer 1024-65535', nargs='?', const=PORT, type=int)
  29. args = parser.parse_args()
  30. if args.dev == True:
  31. IS_DEV = True
  32. ROOT = os.getcwd()
  33. print('\nRunning in developer mode at `{}`'.format(ROOT))
  34. if args.host:
  35. HOST = args.host
  36. if args.port:
  37. PORT = args.port
  38. DIR_TEMPLATES = os.path.join(ROOT, 'static/')
  39. DIR_FILES = os.path.join(ROOT, 'files/')
  40. DATABASE = os.path.join(ROOT, 'db.sqlite')
  41. ui = Bottle()
  42. try:
  43. print('\nOpening database `{}`...'.format(DATABASE))
  44. db = dataset.connect('sqlite:///{}'.format(DATABASE))
  45. except:
  46. print('Unable to open database. Exiting program...')
  47. quit()
  48. pass
  49. SECRAND = SystemRandom() # generator of randomness
  50. CAPTCHA_SESSIONS = [] # captcha cookies
  51. #######################################
  52. # #
  53. # DON'T TOUCH SHIT FROM DOWN HERE #
  54. # #
  55. #######################################
  56. # Create the database tables if they don't exist
  57. for table_type in ['files', 'users', 'captcha', 'session']:
  58. if table_type not in db.tables:
  59. db.create_table(table_type)
  60. #######################################
  61. # Login, sessions and shit #
  62. #######################################
  63. def get_login_creds():
  64. """
  65. Get the username/password combination from a request
  66. Returns a tuple in the form of (username, password)
  67. """
  68. # TODO: Improve robustness of this code
  69. if request.method == 'POST':
  70. username = request.forms.getunicode('username')
  71. password = request.forms.getunicode('password')
  72. if not username:
  73. abort(401, 'You need to be logged in to do this')
  74. return (username, password)
  75. else:
  76. if 'Authorization' in request.headers:
  77. _method, user_pass_combo = request.headers['Authorization'].split()
  78. username, password = user_pass_combo.split(':')
  79. return (username, password)
  80. else:
  81. abort(401)
  82. def check_login(usr, pwd, anon=True):
  83. """
  84. Check for password login, or anonymous login.
  85. `usr` - username for login to check
  86. `pwd` - password for login to check
  87. `anon` - whether or not to allow anonymous logins
  88. Returns: `True` if login is good, `False` if otherwise.
  89. """
  90. if usr == 'anonymous': # Anonymous login
  91. return anon
  92. else: # TODO: Check SQLite DB
  93. result = db['users'].find_one(username=usr)
  94. if result and pwd: # login exists
  95. salt_hash_comb = result['p_hash']
  96. # cipher_num is for future migrations/logins
  97. # 6 = sha-512
  98. cipher_num, salt, login_hash = salt_hash_comb.split('$')
  99. calc_hash = sha512(bytes(salt + pwd, 'utf-8')).hexdigest()
  100. if calc_hash == login_hash:
  101. return True
  102. return False
  103. def check_login_session():
  104. """
  105. Check that a user's session is good by looking at their cookies.
  106. Cookies are generated in start_login_session()
  107. Cookies are removed in end_login_session()
  108. """
  109. login_session = request.get_cookie('login_session')
  110. if login_session and ':' in login_session:
  111. username, session = [x for x in login_session.split(':', 1)]
  112. user_row = db['session'].find_one(user=username, session=session)
  113. if user_row:
  114. match_id = '{}:{}'.format(user_row['user'], user_row['session'])
  115. return match_id == login_session
  116. else:
  117. return False
  118. else:
  119. return False
  120. def remove_session(session_id=None, username=None):
  121. """
  122. Search for a login session in the sqlite database and remove it
  123. if the user or session exists.
  124. For use with internal API only.
  125. `session_id` - Session ID that will be removed
  126. `username` - If `session_id` is not specified, `username` will get session removed
  127. """
  128. assert not (session_id and username), 'You may only pass one parameter at once'
  129. assert username or session_id, 'You must pass a parameter to this function'
  130. results = None
  131. if session_id:
  132. results = db['session'].find(session=session_id)
  133. elif username:
  134. results = db['session'].find(user=username)
  135. else:
  136. abort(500, 'Uh oh. Were you not logged in?')
  137. db.begin()
  138. for row in results:
  139. row_id = row['id']
  140. db['session'].delete(id=row_id)
  141. print('Deleted row {}'.format(row_id))
  142. db.commit()
  143. def start_login_session(usr, remember=False):
  144. """
  145. Gives a user a cookie to start their login session
  146. `usr` - The user that will get a cookie
  147. `remember` - Whether to have the login session "remembered"
  148. """
  149. # Find session for user in database and remove it
  150. remove_session(username=usr)
  151. # Format of the session cookie is `user:session_id`
  152. session_id = sha512(bytes(uuid4().hex, 'utf-8')).hexdigest()
  153. session_cookie = '{}:{}'.format(usr, session_id)
  154. # Add session to database
  155. db.begin()
  156. db['session'].insert({'user': usr, 'session': session_id})
  157. db.commit()
  158. if remember:
  159. expire_time = time.time() + 14 * 24 * 3600 # Expire in 14 days
  160. response.set_cookie('login_session', session_cookie, expires=expire_time)
  161. else:
  162. response.set_cookie('login_session', session_cookie)
  163. def _get_session_username():
  164. """
  165. Get the username of the current session in the request
  166. NOTE: This DOES NOT verify the user is logged in, and should
  167. be used for cosmetic purposes only.
  168. Returns: `username` of the user with a cookie (?)
  169. """
  170. login_session = request.get_cookie('login_session')
  171. if login_session and ':' in login_session:
  172. return login_session.split(':', 1)[0]
  173. else:
  174. return None
  175. def check_captcha(cookie_id, captcha_answer):
  176. """
  177. Check a captcha answer and cookie ID against stored credentials
  178. `cookie_id` - Unique ID of a session
  179. `captcha_answer` - Answer returned by the user
  180. Returns: `True` if the answer matches the known solution
  181. """
  182. for auth_pair in CAPTCHA_SESSIONS:
  183. print(auth_pair)
  184. if auth_pair[0] == cookie_id and auth_pair[1] == captcha_answer:
  185. CAPTCHA_SESSIONS.remove(auth_pair) # Remove correctly solved session, we don't need it anymore
  186. return True
  187. return False
  188. #######################################
  189. # General functions #
  190. #######################################
  191. def generate_etag(param_list):
  192. """
  193. Generate a weak ETag from a list of parameters.
  194. ETAG is generated from concatenating items in param_list,
  195. and hashing the result through an MD5 Hash.
  196. MD5 is used because it is fast. In this case, the ETag does
  197. not have to be secure, so using MD5 is not an issue.
  198. `param_list` - List of things to be hashed into an ETag
  199. Returns: ETag of `param_list`
  200. """
  201. combo = ''
  202. for param in param_list:
  203. combo += str(param)
  204. # 'W/' for weak ETags
  205. return 'W/' + md5(bytes(combo, 'utf-8')).hexdigest()
  206. def get_random_key():
  207. """
  208. Get a file key that isn't already taken
  209. Returns: an unique key
  210. """
  211. key = SECRAND.randint(0, 66 ** 4)
  212. while True:
  213. if not db['files'].find_one(key=key):
  214. return key
  215. key = SECRAND.randint(0, 66 ** 4) # O(1) operation
  216. def sanitize(unsafe_input):
  217. """
  218. Check for unsafe input
  219. """
  220. for character in unsafe_input:
  221. if character not in ALLOWED_CHARS:
  222. return False
  223. return True
  224. def user_pastes(username):
  225. """
  226. Return a dicitonary of a user's pastes
  227. username - the user to get pastes from
  228. """
  229. search_results = db['files'].find(user=username)
  230. data = {}
  231. for paste in search_results:
  232. key = paste['key']
  233. short_id = safeurl.num_encode(key)
  234. data[short_id] = api_file_info(short_id)
  235. return data
  236. #######################################
  237. # Files and shit #
  238. #######################################
  239. def get_file(fileid, mime_type, req=None, download=False, filename=None):
  240. """
  241. Get a file
  242. `fileid` - The unique ID of a file
  243. `mime_type` - Used to identify a file's type
  244. `headers` - If we want to specify special headers
  245. `req` - Internal request object
  246. `download` - Force download of a file
  247. `filename` - Original name of a file
  248. Returns: File
  249. """
  250. file_info = api_file_info(fileid, headers=True)
  251. if download:
  252. return custom_static_file(fileid, root=DIR_FILES, request=req, custom_headers=file_info, mimetype=mime_type, download=filename)
  253. else:
  254. return custom_static_file(fileid, root=DIR_FILES, request=req, custom_headers=file_info, mimetype=mime_type)
  255. def increment_view_count(paste):
  256. """
  257. Increment the view counter of a paste by 1.
  258. `paste` - the short key of a paste
  259. """
  260. long_key = safeurl.num_decode(paste)
  261. row = db['files'].find_one(key=long_key)
  262. db.begin()
  263. db['files'].update({'key': row['key'], 'views': (row['views'] + 1)}, ['key'])
  264. db.commit()
  265. #######################################
  266. # API Stuff #
  267. #######################################
  268. @ui.post('/api/user')
  269. @ui.post('/api/user/')
  270. def api_user_pastes():
  271. """
  272. API for viewing a user's pastes.
  273. User must be logged in to see own pastes.
  274. Returns: Data if the user is logged in, otherwise error 401
  275. """
  276. username, password = get_login_creds()
  277. if check_login(username, password, anon=False):
  278. return user_pastes(username)
  279. elif username == 'anonymous':
  280. abort(401, 'You must have an account to do this')
  281. else:
  282. abort(401, 'You need to log in to do this.')
  283. @ui.delete('/api/file/<fileid>') # REST API
  284. @ui.post('/api/file/rm/<fileid>') # Pleb API
  285. def api_file_delete(fileid):
  286. """
  287. Delete a file by its file id.
  288. Authorization is in the user-pass format.
  289. """
  290. user, password = get_login_creds()
  291. file_info = api_file_info(fileid)
  292. long_key = safeurl.num_decode(fileid)
  293. if check_login(user, password, anon=False) and file_info['user'].lower() == user.lower():
  294. long_key = safeurl.num_decode(fileid)
  295. db.begin()
  296. db['files'].delete(key=long_key)
  297. db.commit()
  298. try:
  299. os.remove(os.path.join(DIR_FILES, fileid))
  300. except:
  301. abort(500, 'Something bad happened. That file isn\'t here :(')
  302. print('[DBG] Deleted file: {}'.format(fileid))
  303. return 'File deleted'
  304. else:
  305. abort(401)
  306. @ui.route('/api/file/<fileid>')
  307. @ui.route('/api/file/get/<fileid>') # TODO: Depreciate
  308. def api_get_file(fileid):
  309. """
  310. Retrieve information about a file or download it.
  311. #: `GET` - Downloads file `fileid`
  312. #: `HEAD` - Downloads information about a file from it's headers.
  313. """
  314. real_key = safeurl.num_decode(fileid)
  315. row = db['files'].find_one(key=real_key) # At this point, `fileid` is safe
  316. if row:
  317. dupe_view = False # Whether this view is a duplicate view
  318. # Check if referer is web UI
  319. if 'Referer' in request.headers:
  320. referer = request.headers['Referer']
  321. if '.' in referer:
  322. base_referer = referer.split('.', 1)
  323. if base_referer[1] == 'hydra.ws/p/{}'.format(fileid):
  324. dupe_view = True
  325. if not dupe_view and request.method == 'GET':
  326. increment_view_count(fileid)
  327. mime_type = row['filetype']
  328. return get_file(fileid, mime_type, req=request)
  329. else:
  330. abort(404)
  331. @ui.route('/api/file/info/<fileid>')
  332. def api_file_info(fileid, headers=False):
  333. """
  334. Get information about a file
  335. `fileid` - Unique file ID
  336. `headers` - To send custom headers or not
  337. Returns: Information about `fileid`, error 404 otherwise
  338. """
  339. file_key = safeurl.num_decode(fileid)
  340. db_result = db['files'].find_one(key=file_key)
  341. if db_result and headers:
  342. # Create ETag for file
  343. last_modified = db_result['time']
  344. filename = db_result['filename']
  345. etag = generate_etag(('static-', filename, last_modified))
  346. return {'X-Paste-User': db_result['user'],
  347. 'X-Paste-Expires': db_result['expires'],
  348. 'X-Paste-Filename': filename,
  349. 'X-Paste-Title': db_result['title'],
  350. 'X-Paste-Views': db_result['views'],
  351. 'Content-Type': db_result['filetype'],
  352. 'Content-Length': db_result['filesize'],
  353. 'Last-Modified': time.strftime('%a, %d %b %Y %H:%M:%S +0000', time.gmtime(last_modified)),
  354. 'ETag': etag}
  355. elif db_result and not headers:
  356. return {'user': db_result['user'],
  357. 'expires': int(db_result['expires']),
  358. 'filename': db_result['filename'],
  359. 'filetype': db_result['filetype'],
  360. 'filesize': int(db_result['filesize']),
  361. 'title': db_result['title'],
  362. 'time': db_result['time'],
  363. 'views': db_result['views'],
  364. 'key': fileid}
  365. else:
  366. abort(404)
  367. @ui.route('/api/upload')
  368. def wrong_get_upload():
  369. abort(400, 'Use a POST, not a GET.')
  370. @ui.post('/api/file/<fileid>') # For file updates
  371. @ui.post('/api/upload') # For initial uploads
  372. def api_upload(fileid=None, web_ui=False, username=None):
  373. """
  374. Function for uploading files to server.
  375. Allows option to modify existing file if uploaded from personal account.
  376. fileid - if non-blank, the short ID of the file to upload
  377. web_ui - whether this upload was via the web ui - assumes correct authentication
  378. username - webui username
  379. """
  380. password = None
  381. if not web_ui:
  382. username, password = get_login_creds()
  383. file_text = request.forms.getunicode('text', default=None)
  384. if username.strip() == '':
  385. abort(401, 'You must specify a login to use this command')
  386. if not web_ui and not check_login(username, password, anon=True):
  387. abort(401, 'Bad login')
  388. # File upload information
  389. upload_time = int(time.time())
  390. upload = request.files.get('upload')
  391. upload_size = None
  392. file_name = None
  393. if upload:
  394. upload_size = request.content_length # For internal purposes, this
  395. # is a rough hack because bottle's FileUpload.content_length is broken
  396. file_name = upload.filename
  397. else:
  398. upload_size = getsizeof(bytes(file_text, 'utf-8'))
  399. # Size + 400 because of metadata included in request header
  400. if file_text and upload_size >= 10000000 + 400:
  401. abort(413, 'You may only upload text up to 10 MB large')
  402. elif not file_text and upload_size >= 150000000 + 400:
  403. abort(413, 'You may only upload files up to 150 MB large')
  404. expires = None
  405. try:
  406. expires = request.forms.get('expires')
  407. delta_time = None
  408. if '+' in expires: # If delta time
  409. delta_time = int(expires.strip()[1:])
  410. expires = int(time.time()) + delta_time
  411. else: # If manual time time
  412. if expires is None: # not specified, give default time
  413. expires = int(time.time()) + 518400 # 518400 sec = 6 days
  414. elif int(expires) == 0: # Never expires
  415. expires = 0
  416. else: # Give custom time
  417. expires = int(expires)
  418. except:
  419. abort(400, 'The "expires" parameter was messed up. Please refer to the API for correct usage.')
  420. # Paste title
  421. paste_title = request.forms.getunicode('title')
  422. if paste_title.strip() == '':
  423. paste_title = file_name if file_name else 'Untitled'
  424. if fileid: # If updating an existing file
  425. file_info = api_file_info(fileid)
  426. if file_info['user'] == 'anonymous': # If upload is anonymous
  427. abort(401, 'You cannot edit anonymous uploads!')
  428. elif username == file_info['user']: # If file belongs to user
  429. file_data = upload.file # TODO So we can also check for 'textfield' variable
  430. save_path = os.path.join(DIR_FILES, fileid)
  431. with open(save_path, 'wb') as f:
  432. f.write(file_data.read())
  433. filesize = os.stat(save_path).st_size
  434. file_info['time'] = int(time.time())
  435. file_info['filesize'] = int(filesize)
  436. file_info['title'] = paste_title
  437. file_info['expires'] = int(expires)
  438. file_info['key'] = safeurl.num_decode(fileid)
  439. #print(file_info)
  440. db.begin()
  441. db['files'].update(file_info, ['key'])
  442. db.commit()
  443. return 'Success! File Modified: {}'.format(fileid)
  444. else: # only other possibility is that the user does not own the file!
  445. abort(401, """You don't have permission to edit that file!\n"""
  446. """It belongs to "{}"!""".format(file_info['user']))
  447. key = get_random_key()
  448. short_key = safeurl.num_encode(key)
  449. # Add file to database
  450. db.begin()
  451. db['files'].insert({'user': username,
  452. 'expires': int(expires), # If 0, never expires. If defined, expires @ epoch time
  453. 'filename': file_name,
  454. 'filetype': None,
  455. 'filesize': int(0),
  456. 'time': upload_time,
  457. 'views': 0,
  458. 'key': key,
  459. 'title': paste_title})
  460. save_path = os.path.join(DIR_FILES, short_key)
  461. with open(save_path, 'wb') as save_file:
  462. if upload: # If we're using a file
  463. save_file.write(upload.file.read())
  464. else:
  465. save_file.write(bytes(file_text, 'utf-8'))
  466. # Detect Mime or get paste
  467. mime_type = request.forms.getunicode('mimetype')
  468. if not mime_type or mime_type == 'auto':
  469. mime_type = magic.from_file(save_path, mime=True).decode('utf-8')
  470. row_id = db['files'].find_one(key=key)['id']
  471. filesize = os.stat(save_path).st_size
  472. #print('Editing MIME type to {}'.format(mime_type))
  473. db['files'].update({'id': row_id, 'filetype': mime_type, 'filesize': filesize}, ['id'])
  474. db.commit()
  475. return 'Success! File ID: {}'.format(short_key)
  476. #######################################
  477. # Web UI #
  478. #######################################
  479. @ui.route('/user')
  480. @ui.route('/user/')
  481. def ui_user_pastes():
  482. """
  483. Page to show a user their pastes.
  484. """
  485. if not check_login_session():
  486. redirect('/login?redirect=/user')
  487. #abort(401, 'Authorization required for this page, please login.')
  488. username = _get_session_username()
  489. pastes = user_pastes(username)
  490. return gen_page(os.path.join(DIR_TEMPLATES, 'userpastes.tpl'),
  491. {'pastes': pastes,
  492. 'username': username})
  493. @ui.route('/user/changepass')
  494. @ui.post('/user/changepass')
  495. def user_change_password():
  496. """
  497. Change a users's password
  498. """
  499. if not check_login_session():
  500. redirect('/login')
  501. data = {'error': None,
  502. 'success': None}
  503. username = _get_session_username()
  504. data['username'] = username
  505. if request.method == 'POST':
  506. current = request.forms.getunicode('pass-current')
  507. pass1 = request.forms.getunicode('pass-new')
  508. pass2 = request.forms.getunicode('pass-confirm')
  509. if not check_login(username, current, anon=False):
  510. data['error'] = 'Incorrect current password. Please try again.'
  511. elif pass1 != pass2:
  512. data['error'] = 'Passwords don\'t match. Please try again.'
  513. elif pass1 == current:
  514. data['error'] = 'Current and new password match'
  515. elif len(pass1) <= 6:
  516. data['error'] = 'Password too weak. Try using more than 6 characters.'
  517. else:
  518. salt = uuid4().hex
  519. pass_hash = sha512(bytes(salt + pass1, 'utf-8')).hexdigest()
  520. hash_salt_comb = '$6$' + salt + '$' + pass_hash
  521. db.begin()
  522. db['users'].update({'username': username, 'p_hash': hash_salt_comb}, ('username'))
  523. db.commit()
  524. data['success'] = True
  525. return gen_page(os.path.join(DIR_TEMPLATES, 'changepass.tpl'), data)
  526. @ui.route('/delete/<fileid>')
  527. @ui.post('/delete/<fileid>')
  528. def ui_delete_file(fileid):
  529. """
  530. Webpage to allow users to delte files
  531. """
  532. if not check_login_session():
  533. redirect('/login?redirect=/delete/{}'.format(fileid))
  534. #abort(401, 'Authorization required for this page, please login.')
  535. username = _get_session_username()
  536. data = {'deleted': False,
  537. 'warning': False,
  538. 'fileid': fileid}
  539. file_info = api_file_info(fileid)
  540. data['pastetitle'] = file_info['title']
  541. if username != file_info['user']:
  542. abort(401, 'You aren\'t allowed to delete this file!')
  543. if request.method == 'POST':
  544. delete = request.forms.getunicode('delete')
  545. print('delete {}'.format(delete))
  546. if delete:
  547. data['deleted'] = True
  548. long_key = safeurl.num_decode(fileid)
  549. db.begin()
  550. db['files'].delete(key=long_key)
  551. db.commit()
  552. try:
  553. os.remove(os.path.join(DIR_FILES, fileid))
  554. except:
  555. abort(500, 'Something bad happened. That file isn\'t here :(')
  556. print('[DBG] Deleted file: {}'.format(fileid))
  557. else:
  558. redirect('/user')
  559. elif request.method == 'GET':
  560. data['warning'] = True
  561. else:
  562. pass
  563. return gen_page(os.path.join(DIR_TEMPLATES, 'deletefile.tpl'), data)
  564. @ui.route('/logout')
  565. def end_login_session():
  566. """
  567. End's a user's login session. Also removes their login cookie.
  568. """
  569. if not check_login_session():
  570. abort(400, 'You need to be logged in to do this!')
  571. # Get username of user, delete cookie, and remove session from database
  572. username = _get_session_username()
  573. remove_session(username=username)
  574. response.delete_cookie('login_session')
  575. redirect('/login')
  576. @ui.route('/uploadcombined') # temporary redirect for fixing links
  577. def upload_redirect():
  578. redirect('/upload')
  579. @ui.route('/upload')
  580. def web_upload_page():
  581. page_data = {}
  582. isFile = request.query.file
  583. if isFile:
  584. page_data['isFile'] = 'true'
  585. else:
  586. page_data['isFile'] = 'false'
  587. username = _get_session_username()
  588. if username:
  589. page_data['username'] = username
  590. return gen_page(os.path.join(DIR_TEMPLATES, 'upload.tpl'), page_data)
  591. @ui.post('/upload') # Combined upload page
  592. def web_upload_post():
  593. cookie_username = _get_session_username()
  594. if cookie_username: # User is logged in w/ cookies
  595. if check_login_session():
  596. output = api_upload(web_ui=True, username=cookie_username)
  597. if 'Success' in output:
  598. key = output.split(':')[1].strip()
  599. data = {'key': key,
  600. 'username': cookie_username}
  601. return gen_page(os.path.join(DIR_TEMPLATES, 'uploadsuccess.tpl'), data)
  602. else:
  603. output = api_upload()
  604. if 'Success' in output:
  605. key = output.split(':')[1].strip()
  606. data = {'key': key}
  607. return gen_page(os.path.join(DIR_TEMPLATES, 'uploadsuccess.tpl'), data)
  608. @ui.route('/register')
  609. @ui.post('/register') # Receiving registration data
  610. def ui_register():
  611. """
  612. Register a user with the web frontent ui
  613. """
  614. reg_data = {'success_text': None,
  615. 'error_text': None}
  616. if request.method == 'POST': # see if we need to verify registration
  617. username = request.forms.getunicode('username')
  618. password = request.forms.getunicode('password')
  619. password_confirm = request.forms.getunicode('password-confirm')
  620. captcha_answer = request.forms.getunicode('captcha-challenge')
  621. captcha_id = request.cookies.getunicode('captcha_id')
  622. email = request.forms.getunicode('email') or None
  623. captcha_result = check_captcha(captcha_id, captcha_answer)
  624. if not sanitize(username):
  625. reg_data['error_text'] = """Usernames can only contain the """ \
  626. """following characters: {}""".format(ALLOWED_CHARS)
  627. elif len(username) < 2 or len(username) > 32:
  628. reg_data['error_text'] = 'Your username\'s length must be between 2 and 32 characters inclusive.'
  629. elif password != password_confirm:
  630. reg_data['error_text'] = "Those passwords don't match. Please try again."
  631. elif not captcha_result:
  632. reg_data['error_text'] = "Sorry, but you failed the captcha, please try again."
  633. elif len(password) <= 6:
  634. reg_data['error_text'] = 'Password too weak. Please try using more than 6 characters.'
  635. elif len(password) > 128:
  636. reg_data['error_text'] = """Your password is really long! Try again with a password """\
  637. """that is 128 characters or fewer."""
  638. else:
  639. # Check if username already exists
  640. taken = False
  641. if len(db['users']) > 0:
  642. users = db['users'].table
  643. query = users.select(users.c.username.ilike('%' + username + '%'))
  644. result = db.query(query)
  645. # See if there are any results
  646. for row in result:
  647. taken = True
  648. break
  649. if taken:
  650. reg_data['error_text'] = 'Sorry, but the username {} is already taken.'.format(username)
  651. elif username.lower().strip() == 'anonymous':
  652. reg_data['error_text'] = 'You are legion. You cannot register with "anonymous".'
  653. elif username.lower().strip() == 'root':
  654. reg_data['error_text'] = 'You must be a superuser to do that ;)'
  655. elif email and not ('@' in email): # There are better ways to do this, but whatever
  656. reg_data['error_text'] = 'That email is invalid. Please try again.'
  657. else:
  658. joined = int(time.time())
  659. salt = uuid4().hex
  660. p_hash = sha512(bytes(salt + password, 'utf-8')).hexdigest()
  661. # TODO: Use correct hash naming scheme with 3$salt$password
  662. hash_salt_comb = '$' + salt + '$' + p_hash
  663. db.begin()
  664. db['users'].insert({'username': username,
  665. 'joined': joined,
  666. 'pastes': 0,
  667. 'p_hash': hash_salt_comb,
  668. 'email': email})
  669. db.commit()
  670. print('Registered user: {}'.format(username))
  671. reg_data['success_text'] = username
  672. # Log user in and stuff
  673. start_login_session(username)
  674. reg_data['username'] = username
  675. reg_data['captcha_data'] = captcha_provider()
  676. return gen_page(os.path.join(DIR_TEMPLATES, 'register.tpl'), reg_data)
  677. def captcha_provider():
  678. """
  679. Generate random CAPTCHA using skimpyGimpy, give use CAPTCHA ID Cookie, and
  680. store CAPTCHA credential in database.
  681. """
  682. # Generate a random string 4 characters long
  683. pass_phrase = ''.join(SECRAND.choice(string.ascii_uppercase + string.digits) for _ in range(4))
  684. # Yes; use MD5 because the cookie doesn't have to be cryptographically secure.
  685. captcha_cookie_id = md5(bytes(str(uuid4()), 'utf-8')).hexdigest()
  686. # If there are already 10 sessions for creating cookies, expire the first session
  687. if len(CAPTCHA_SESSIONS) >= 10:
  688. _ = CAPTCHA_SESSIONS.pop(0)
  689. CAPTCHA_SESSIONS.append((captcha_cookie_id, pass_phrase))
  690. # Generate CAPTCHA with skimpyGimpy
  691. captcha_gen = skimpyAPI.Pre(pass_phrase, speckle=0.33, scale=1.33, color='#fff')
  692. captcha_test = captcha_gen.data()
  693. response.set_cookie('captcha_id', captcha_cookie_id)
  694. #print('Cookie {} coresponds to solution {}'.format(captcha_cookie_id, pass_phrase))
  695. return captcha_test
  696. @ui.route('/login')
  697. @ui.post('/login')
  698. def ui_login():
  699. """
  700. Login for UI
  701. """
  702. login_data = {'success': False,
  703. 'failure': False,
  704. 'anon_failure': False,
  705. 'login_needed': False,
  706. 'redirect': None}
  707. redirect_url = request.query.redirect
  708. if request.method == 'POST': # see if we need to verify registration
  709. username, password = get_login_creds()
  710. if check_login(username, password, anon=False):
  711. login_data['success'] = username
  712. if request.forms.getunicode('remember'):
  713. start_login_session(username, remember=True)
  714. else:
  715. start_login_session(username)
  716. if request.forms.getunicode('redirect'):
  717. redirect(request.forms.getunicode('redirect'))
  718. elif username.lower() == 'anonymous':
  719. login_data['anon_failure'] = True
  720. elif redirect_url:
  721. login_data['login_needed'] = True
  722. login_data['redirect'] = request.query.redirect
  723. else:
  724. login_data['failure'] = True
  725. else:
  726. if redirect_url:
  727. login_data['login_needed'] = True
  728. login_data['redirect'] = redirect_url
  729. return gen_page(os.path.join(DIR_TEMPLATES, 'login.tpl'), login_data)
  730. @ui.route('/p/<paste>')
  731. def get_ui_paste(paste):
  732. # Get file_data first to handle 404s
  733. file_data = api_file_info(paste)
  734. if request.method == 'GET': # Fix for HEAD to inflate view count
  735. increment_view_count(paste)
  736. file_data['short_key'] = paste
  737. file_data['file_internal_src'] = os.path.join(DIR_FILES, paste)
  738. file_data['file_external_src'] = '/api/file/{}'.format(paste)
  739. if not file_data['title']:
  740. file_data['title'] = 'Untitled Paste'
  741. return gen_page(os.path.join(DIR_TEMPLATES, 'paste.tpl'), file_data)
  742. @ui.error(404)
  743. def error404(error_text):
  744. return '404 - Sorry man, but the file isn\'t here :('
  745. def gen_page(template_path, data={}):
  746. """
  747. Generates a page with the headers and footers, along with the data dictionary given
  748. template_path - abosolute path of the template to use
  749. data - any page-specific data that needs to be given to the page
  750. Optional Keys:
  751. username - username of logged in user
  752. """
  753. data['top'] = os.path.join(DIR_TEMPLATES, 'top.html')
  754. data['bottom'] = os.path.join(DIR_TEMPLATES, 'bottom.html')
  755. data['footer'] = os.path.join(DIR_TEMPLATES, 'footer.html')
  756. data['stylesheets'] = tuple()
  757. data['scripts'] = tuple()
  758. data['og_image'] = '/static/favicon.ico'
  759. data['card_type'] = 'summary'
  760. if 'username' not in data:
  761. username = _get_session_username()
  762. if username:
  763. data['username'] = username
  764. else:
  765. data['username'] = None
  766. # Standard date time
  767. if 'time' in data:
  768. mod_time = data['time']
  769. time_diff = int(time.time()) - mod_time
  770. suffix = None
  771. time_unit = 0
  772. # Fast natural date implementation
  773. if time_diff <= 1 or time_diff <= 60:
  774. time_unit = time_diff # Diff in seconds
  775. if time_diff == 1:
  776. suffix = 'second'
  777. else:
  778. suffix = 'seconds'
  779. elif time_diff > 60 and time_diff < 3600:
  780. # 60 secs/min
  781. time_unit = time_diff // 60
  782. if time_diff < 120:
  783. suffix = 'minute'
  784. else:
  785. suffix = 'minutes'
  786. elif time_diff >= 3600 and time_diff < 86400:
  787. # 60^2 sec/hr
  788. time_unit = time_diff // (60 * 60)
  789. if time_diff < 7200:
  790. suffix = 'hour'
  791. else:
  792. suffix = 'hours'
  793. else:
  794. # 60^2 * 24 sec/day
  795. time_unit = time_diff // (60 * 60 * 24)
  796. suffix = 'days'
  797. data['time'] = '{} {} ago'.format(time_unit, suffix)
  798. return template(template_path, data)
  799. @ui.route('/') # Index Page
  800. @ui.route('/<filename>') # Favicon.ico workaround
  801. @ui.route('/static/<filename:path>') # All other static files
  802. def static_route(filename=None):
  803. """
  804. Route files in DIR_TEMPLATES when using --dev mode.
  805. Route homepage whenever using production setup.
  806. Also routes the index file (found in `/`)
  807. """
  808. print('Local Routing with {}'.format(filename))
  809. if not filename: # Frontpage
  810. return gen_page(os.path.join(DIR_TEMPLATES, 'frontpage.tpl'))
  811. elif filename == 'favicon.ico': # Favicon workaround
  812. return static_file(filename, root=DIR_TEMPLATES)
  813. else:
  814. return static_file(filename, root=DIR_TEMPLATES)
  815. def prune_files(interval, _db):
  816. """
  817. Checks which files have expired given int(interval) as a second-format cooldown.
  818. """
  819. while True:
  820. time.sleep(interval)
  821. print('Cleaning shit up')
  822. now = int(time.time())
  823. try:
  824. # This is the fastest way to extract all files' information from the DB,
  825. # so the transaction is unlikely to get interrupted and crash!
  826. all_files = [fil for fil in _db['files'].all()]
  827. except:
  828. continue
  829. _db.begin()
  830. for _file in all_files:
  831. expires = int(_file['expires'])
  832. if expires <= now and expires:
  833. _db['files'].delete(id=_file['id'])
  834. _db.commit()
  835. key = safeurl.num_encode(_file['key'])
  836. try:
  837. os.remove(os.path.join(DIR_FILES, key))
  838. except:
  839. continue
  840. print("[DBG] Deleted file: %i\n\t%s" % (_file['id'], key))
  841. _db.commit()
  842. print('Shit cleaned up')
  843. if __name__ == '__main__':
  844. # check to make sure `files` folder exists, if not, then create it.
  845. if not os.path.exists(DIR_FILES):
  846. os.makedirs(DIR_FILES)
  847. # Starting the prune_files(interval) thread.
  848. t_prune = Thread(target=prune_files, args=(CLEAN_INTERVAL, db))
  849. t_prune.start()
  850. run(ui, host=HOST, port=PORT)