# coding=utf-8 import json import _mysql import time from framework import * from database import * from post import * def api(self, path_split): if len(path_split) > 2: try: self.output = api_process(self, path_split) except APIError as e: self.output = api_error("error", e.message) except UserError as e: self.output = api_error("failed", e.message) except Exception as e: import sys import traceback exc_type, exc_value, exc_traceback = sys.exc_info() detail = ["%s : %s : %s : %s" % (os.path.basename( o[0]), o[1], o[2], o[3]) for o in traceback.extract_tb(exc_traceback)] self.output = api_error("exception", str(e), str(type(e)), detail) else: self.output = api_error("error", "No method specified") def api_process(self, path_split): formdata = self.formdata ip = self.ip t = time.time() method = path_split[2] values = {'state': 'success'} validated = False staff_account = None token = formdata.get("token") if token: staff_account = validateSession(token) if not staff_account: self.output = api_error("error", "Session expired") if staff_account: validated = True if 'session_id' in staff_account: renewSession(staff_account['session_id']) UpdateDb('UPDATE `staff` SET `lastactive` = ' + str(timestamp() ) + ' WHERE `id` = ' + staff_account['id'] + ' LIMIT 1') if validated == False: if method == 'login': if 'username' in self.formdata and 'password' in self.formdata: staff_account = verifyPasswd( formdata.get("username"), formdata.get("password")) if staff_account: session_uuid = newSession(staff_account['id']) values["token"] = session_uuid UpdateDb('DELETE FROM `logs` WHERE `timestamp` < ' + str(timestamp() - 604800)) # one week else: logAction('', 'Failed log-in. Username:'+_mysql.escape_string( self.formdata['username'])+' IP:'+self.ip) raise APIError("Incorrect username/password.") else: raise APIError("Bad request") else: raise APIError("Not authenticated") else: if method == 'news': news = FetchAll( "SELECT * FROM `news` WHERE type = 1 ORDER BY `timestamp` DESC") values['news'] = news elif method == 'post': board = setBoard(formdata.get("board")) if 'id' in formdata: id = formdata.get('id') post = FetchOne("SELECT `id`, `boardid`, `parentid`,`timestamp`, `name`, `tripcode`, `email` ,`subject`,`message`,`file`,`thumb`, INET6_NTOA(`ip`) as ip,`IS_DELETED` AS `deleted`, `bumped`, `last`, `locked` FROM `posts` WHERE `id` = '" + _mysql.escape_string(id) + "' AND `boardid` = '" + _mysql.escape_string(board["id"]) + "'") post['id'] = int(post['id']) post['bumped'] = int(post['bumped']) post['deleted'] = int(post['deleted']) post['last'] = int(post['last']) post['locked'] = int(post['locked']) post['parentid'] = int(post['parentid']) post['timestamp'] = int(post['timestamp']) post['boardid'] = int(post['boardid']) values['post'] = post if 'parentid' in formdata: id = formdata.get('parentid') posts = FetchAll("SELECT `id`, `boardid`, `parentid`,`timestamp`, `name`, `tripcode`, `email` ,`subject`,`message`,`file`,`thumb`, INET6_NTOA(`ip`) as ip,`IS_DELETED` AS `deleted`, `bumped`, `last`, `locked` FROM `posts` WHERE `parentid` = '" + _mysql.escape_string(id) + "' AND `boardid` = '" + _mysql.escape_string(board["id"]) + "'") for post in posts: post['id'] = int(post['id']) post['bumped'] = int(post['bumped']) post['deleted'] = int(post['deleted']) post['last'] = int(post['last']) post['locked'] = int(post['locked']) post['parentid'] = int(post['parentid']) post['timestamp'] = int(post['timestamp']) post['boardid'] = int(post['boardid']) values['posts'] = posts elif method == 'threadlist': data_board = formdata.get('dir') data_offset = formdata.get('offset') data_limit = formdata.get('limit') data_replies = formdata.get('replies') offset = 0 limit = 50 numreplies = 2 if not data_board: raise APIError("Missing parameters") if data_limit: try: limit = int(data_limit) except ValueError: raise APIError("Limit must be numeric") if data_offset: try: offset = int(data_offset) except ValueError: raise APIError("Offset must be numeric") if data_replies: try: numreplies = int(data_replies) except ValueError: raise APIError("Replies must be numeric") if data_replies and limit > 30: raise APIError("Maximum limit is 30") board = setBoard(data_board) #sql = "SELECT id, timestamp, bumped, timestamp_formatted, name, tripcode, email, subject, message, file, thumb FROM posts WHERE boardid = %s AND parentid = 0 AND IS_DELETED = 0 ORDER BY bumped DESC LIMIT %d" % (board['id'], limit) sql = "SELECT INET6_NTOA(p.ip) AS ip, p.IS_DELETED AS deleted, p.id, p.timestamp, p.bumped, p.expires, p.expires_formatted, p.timestamp_formatted, p.name, p.tripcode, p.email, p.subject, p.message, p.file, p.file_size, p.image_width, p.image_height, p.thumb, p.thumb_height, p.thumb_width, p.locked, coalesce(x.count,0) AS total_replies, coalesce(x.files,0) AS total_files FROM `posts` AS p LEFT JOIN (SELECT parentid, count(1) as count, count(nullif(file, '')) as files FROM `posts` WHERE boardid = %(board)s GROUP BY parentid) AS x ON p.id=x.parentid WHERE p.parentid = 0 AND p.boardid = %(board)s ORDER BY `bumped` DESC LIMIT %(limit)d OFFSET %(offset)d" % { 'board': board["id"], 'limit': limit, 'offset': offset} threads = FetchAll(sql) if numreplies: for thread in threads: lastreplies = FetchAll("SELECT INET6_NTOA(ip) AS ip, id, timestamp, timestamp_formatted, name, tripcode, email, subject, message, file, file_size, image_height, image_width, thumb, thumb_width, thumb_height, IS_DELETED AS deleted FROM `posts` WHERE parentid = %s AND boardid = %s ORDER BY `timestamp` DESC LIMIT %d" % ( thread['id'], board['id'], numreplies)) lastreplies = lastreplies[::-1] thread['id'] = int(thread['id']) thread['timestamp'] = int(thread['timestamp']) thread['bumped'] = int(thread['bumped']) thread['expires'] = int(thread['expires']) thread['total_replies'] = int(thread['total_replies']) thread['total_files'] = int(thread['total_files']) thread['file_size'] = int(thread['file_size']) thread['image_width'] = int(thread['image_width']) thread['image_height'] = int(thread['image_height']) thread['thumb_width'] = int(thread['thumb_width']) thread['thumb_height'] = int(thread['thumb_height']) thread['locked'] = int(thread['locked']) thread['replies'] = [] for post in lastreplies: post['deleted'] = int(post['deleted']) post['id'] = int(post['id']) post['timestamp'] = int(post['timestamp']) if post['deleted']: empty_post = {'id': post['id'], 'deleted': post['deleted'], 'timestamp': post['timestamp'], } thread['replies'].append(empty_post) else: post['file_size'] = int(post['file_size']) post['image_width'] = int(post['image_width']) post['image_height'] = int(post['image_height']) post['thumb_width'] = int(post['thumb_width']) post['thumb_height'] = int(post['thumb_height']) post['message'] = post['message'].decode( 'utf-8', 'replace') thread['replies'].append(post) values['threads'] = threads elif method == 'reports': if len(path_split) > 3: if path_split[3] == 'ignore': report_id = formdata.get("id") UpdateDb("DELETE FROM `reports` WHERE `id` = '" + _mysql.escape_string(report_id)+"'") else: values['state'] = "error" else: reports = FetchAll( "SELECT id, timestamp, timestamp_formatted, postid, parentid, link, board, INET6_NTOA(ip) AS ip, reason, INET6_NTOA(repip) AS repip FROM `reports` ORDER BY `timestamp` DESC") values['reports'] = reports elif method == 'logs': logs = FetchAll("SELECT * FROM `logs` ORDER BY `timestamp` DESC") values['logs'] = logs elif method == 'staffPosts': posts = FetchAll( "SELECT * FROM `news` WHERE type = '0' ORDER BY `timestamp` DESC") values['posts'] = posts elif method == 'login': values['token'] = token elif method == 'members': members = FetchAll( "SELECT * FROM `staff` ORDER BY lastactive DESC") values['members'] = members elif method == 'stats': report_count = FetchOne("SELECT COUNT(id) FROM `reports`") try: with open('stats.json', 'r') as f: out = json.load(f) values['stats'] = out except ValueError: values['stats'] = None raise APIError("Stats error") values['stats']['reportCount'] = report_count['COUNT(id)'] else: raise APIError("Invalid method") values['time'] = int(t) return json.dumps(values, sort_keys=True, separators=(',', ':')) def api_error(errtype, msg, type=None, detail=None): values = {'state': errtype, 'message': msg} if type: values['type'] = type if detail: values['detail'] = detail return json.dumps(values) def newSession(staff_id): import uuid session_uuid = uuid.uuid4().hex param_session_id = _mysql.escape_string(session_uuid) param_expires = timestamp() + Settings.SESSION_TIME param_staff_id = int(staff_id) InsertDb("INSERT INTO `session` (`session_id`, `expires`, `staff_id`) VALUES (UNHEX('%s'), %d, %d)" % (param_session_id, param_expires, param_staff_id)) return session_uuid def validateSession(session_id): cleanSessions() param_session_id = _mysql.escape_string(session_id) param_now = timestamp() session = FetchOne( "SELECT HEX(session_id) as session_id, id, username, rights, added FROM `session` " "INNER JOIN `staff` ON `session`.`staff_id` = `staff`.`id` " "WHERE `session_id` = UNHEX('%s')" % (param_session_id)) if session: return session return None def renewSession(session_id): param_session_id = _mysql.escape_string(session_id) param_expires = timestamp() + Settings.SESSION_TIME UpdateDb("UPDATE `session` SET expires = %d WHERE session_id = UNHEX('%s')" % (param_expires, param_session_id)) def deleteSession(session_id): param_session_id = _mysql.escape_string(session_id) UpdateDb("DELETE FROM `session` WHERE session_id = UNHEX('%s')" % param_session_id) def cleanSessions(): param_now = timestamp() UpdateDb("DELETE FROM `session` WHERE expires <= %d" % param_now) def logAction(staff, action): InsertDb("INSERT INTO `logs` (`timestamp`, `staff`, `action`) VALUES (" + str(timestamp()) + ", '" + _mysql.escape_string(staff) + "\', \' [API] " + _mysql.escape_string(action) + "\')") def verifyPasswd(username, passwd): import argon2 ph = argon2.PasswordHasher() param_username = _mysql.escape_string(username) staff_account = FetchOne( "SELECT * FROM staff WHERE username = '%s'" % param_username) if not staff_account: return None try: ph.verify(staff_account['password'], passwd) except argon2.exceptions.VerifyMismatchError: return None except argon2.exceptions.InvalidHash: raise UserError("Hash obsoleto o inválido. Por favor contacte al administrador.") if ph.check_needs_rehash(staff_account['password']): param_new_hash = ph.hash(staff_acount['password']) UpdateDb("UPDATE staff SET password = '%s' WHERE id = %s" % (param_new_hash, staff_account['id'])) return staff_account class APIError(Exception): pass