# 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, e:
self.output = api_error("error", e.message)
except UserError, e:
self.output = api_error("failed", e.message)
except Exception, 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.environ["REMOTE_ADDR"]
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.environ["REMOTE_ADDR"])
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.keys():
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.keys():
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