this page to renew it.', // status 1
ERR_REFUNDED = 'This Pass has been refunded and disabled. You cannot use it anymore.', // status 2
ERR_DISPUTED = 'This Pass has a disputed payment. You cannot use it until the dispute is resolved.', // status 3
ERR_REVOKED_SPAM = 'This Pass has been revoked due to spamming, which is a violation of the Terms of Use.', // status 4
ERR_REVOKED_ILLEGAL = 'This Pass has been revoked due to illegal content being posted, which is a violaton of the Terms of Use.' // status 5
;
private function error($msg) {
$this->renderResponse(self::AUTH_ERROR, $msg);
}
private function renderResponse($status, $msg = null) {
if ($this->is_xhr) {
header('Content-type: application/json');
echo json_encode(array('status' => $status, 'message' => $msg));
}
else {
$this->auth_status = $status;
$this->message = $msg;
require_once(self::VIEW_TPL);
}
die();
}
private function pretty_duration($sec) {
$duration = '';
$hours = (int)($sec / 3600);
$minutes = (int)($sec / 60);
if ($hours) {
$duration .= str_pad($hours, 2, '0', STR_PAD_LEFT) . ' hour';
if ($hours != 1) {
$duration .= 's';
}
$duration .= ' ';
}
if ($minutes) {
$minutes = (int)(($sec / 60) % 60);
$duration .= str_pad($minutes, 2, '0', STR_PAD_LEFT). ' minute';
if ($minutes != 1) {
$duration .= 's';
}
}
$seconds = intval($sec % 60);
return $duration;
}
private function get_csrf_token() {
return bin2hex(openssl_random_pseudo_bytes(16));
}
private function validate_referer() {
if (!isset($_SERVER['HTTP_REFERER']) || $_SERVER['HTTP_REFERER'] === '') {
return;
}
if (!preg_match('/^https:\/\/sys\.(4chan|4channel)\.org(\/|$)/', $_SERVER['HTTP_REFERER'])) {
$this->error(self::ERR_BAD_REQUEST);
}
}
private function validate_csrf() {
if (!isset($_COOKIE['csrf']) || !isset($_POST['csrf'])
|| $_COOKIE['csrf'] === '' || $_POST['csrf'] === '') {
$this->error(self::ERR_BAD_REQUEST);
}
if ($_COOKIE['csrf'] !== $_POST['csrf']) {
$this->error(self::ERR_BAD_REQUEST);
}
}
private function validate_auth_flood($long_ip) {
if (!$long_ip) {
return;
}
$query = "SELECT COUNT(ip) FROM user_actions WHERE ip = $long_ip AND action = 'fail_pass_auth' AND time >= DATE_SUB(NOW(), INTERVAL 1 HOUR)";
$res = mysql_global_call($query);
if (!$res) {
return;
}
$count = (int)mysql_fetch_row($res)[0];
if ($count >= LOGIN_FAIL_HOURLY) {
$this->error(self::ERR_FLOOD);
}
}
private function register_auth_failure($long_ip) {
if (!$long_ip) {
return;
}
$query = "INSERT INTO user_actions (ip, board, action, time) VALUES(%d, '', 'fail_pass_auth', NOW())";
$res = mysql_global_call($query, $long_ip);
}
private function convert_new_pass_status($user_hash, $hashed_pin) {
$table = self::PASS_TABLE;
$query = "UPDATE $table SET pin = '%s', status = 0 WHERE user_hash = '%s' AND status = 6 LIMIT 1";
mysql_global_call($query, $hashed_pin, $user_hash);
$this->set_cookie('pass_email', '', -1);
}
private function convert_delayed_pass_status($user_hash, $hashed_pin) {
$table = self::PASS_TABLE;
$query = "UPDATE $table SET pin = '%s', status = 0, expiration_date = NOW() + INTERVAL 1 YEAR WHERE user_hash = '%s' AND status = 7 LIMIT 1";
mysql_global_call($query, $hashed_pin, $user_hash);
}
private function set_cookie($name, $value, $expire, $secure = false, $http_only = false) {
setcookie($name, $value, $expire, '/', '.' . THIS_DOMAIN, $secure, $http_only);
}
private function clear_cookies() {
$cookie_time = $_SERVER['REQUEST_TIME'] - 3600;
$this->set_cookie('pass_id', null, $cookie_time, true, true);
$this->set_cookie('pass_enabled', null, $cookie_time);
}
private function get_random_base64bytes($length = 64) {
$data = openssl_random_pseudo_bytes($length);
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
private function get_salt() {
$salt = file_get_contents('/www/keys/2014_admin.salt');
if (!$salt) {
$this->error(sprintf(self::ERR_GENERIC, 'gs'));
}
return $salt;
}
/**
* Login
*/
private function authenticate() {
$this->validate_referer();
$table = self::PASS_TABLE;
$time_now = time();
// Token
if (!isset($_POST['id']) || $_POST['id'] === '') {
$this->error(self::ERR_EMPTY_FIELD);
}
if (strlen($_POST['id']) != 10) {
$this->error(self::ERR_TOKEN_LEN);
}
$id = $_POST['id'];
// Pin
if (!isset($_POST['pin']) || $_POST['pin'] === '') {
$this->error(self::ERR_EMPTY_FIELD);
}
$pin = $_POST['pin'];
// ---
$ip = $_SERVER['REMOTE_ADDR'];
$long_ip = ip2long($ip);
$this->validate_auth_flood($long_ip);
// ---
$plain_pin = $pin;
$pin = crypt($pin, substr($id, 4, 9));
$query = "SELECT * FROM $table WHERE user_hash = '%s' AND (pin = '%s' OR pin = '%s') LIMIT 1";
$res = mysql_global_call($query, $id, $pin, $plain_pin);
if (!$res) {
$this->error(self::ERR_DB);
}
if (mysql_num_rows($res) !== 1) {
$this->register_auth_failure($long_ip);
$this->error(self::ERR_BAD_AUTH);
}
$pass = mysql_fetch_assoc($res);
if (!$pass) {
$this->error(sprintf(self::ERR_GENERIC, 'mfa1'));
}
$last_used = strtotime($pass['last_used']);
$last_ip_mask = ip2long($pass['last_ip']) & (~65535);
$ip_mask = $long_ip & (~65535);
if ($last_ip_mask !== 0 && ($time_now - $last_used) < PASS_TIMEOUT && $last_ip_mask != $ip_mask) {
$remaining = $this->pretty_duration(PASS_TIMEOUT - ($time_now - $last_used));
$this->error(sprintf(self::ERR_IN_USE, $remaining));
}
switch ($pass['status']){
case 0:
break;
case 1:
$this->clear_cookies();
$this->error(sprintf(self::ERR_EXPIRED, $pass['pending_id']));
break;
case 2:
$this->clear_cookies();
$this->error(self::ERR_REFUNDED);
break;
case 3:
$this->clear_cookies();
$this->error(self::ERR_DISPUTED);
break;
case 4:
$this->clear_cookies();
$this->error(self::ERR_REVOKED_SPAM);
break;
case 5:
$this->clear_cookies();
$this->error(self::ERR_REVOKED_ILLEGAL);
break;
case 6:
$this->convert_new_pass_status($pass['user_hash'], $pin);
break;
case 7:
$this->convert_delayed_pass_status($pass['user_hash'], $pin);
break;
}
// Update country
$geo_data = GeoIP2::get_country($ip);
if ($geo_data && isset($geo_data['country_code'])) {
$country_code = mysql_real_escape_string($geo_data['country_code']);
}
else {
$country_code = 'XX';
}
$update_country = ", last_country = '$country_code'";
$query = "UPDATE $table SET last_ip = '%s', last_used = NOW() $update_country WHERE user_hash = '%s' AND last_ip != '%s' AND status = 0 LIMIT 1";
mysql_global_call($query, $ip, $id, $ip);
// Update session id
if (!$pass['session_id']) {
$pass_session = $this->get_random_base64bytes(32);
if (!$pass_session) {
$this->error(sprintf(self::ERR_GENERIC, 'grb'));
}
$query = "UPDATE $table SET session_id = '$pass_session' WHERE user_hash = '%s' AND status = 0 LIMIT 1";
mysql_global_call($query, $id);
}
else {
$pass_session = $pass['session_id'];
}
$admin_salt = $this->get_salt();
$hashed_pass_session = substr(hash('sha256', $pass_session . $admin_salt), 0, 32);
if (!$hashed_pass_session) {
$this->error(sprintf(self::ERR_GENERIC, 'hps'));
}
if (isset($_POST['long_login'])) {
$cookie_time = $time_now + 31556900;
}
else {
$cookie_time = $time_now + 86400;
}
$this->set_cookie('pass_id', "$id.$hashed_pass_session", $cookie_time, true, true);
$this->set_cookie('pass_enabled', '1', $cookie_time);
$this->renderResponse(self::AUTH_SUCCESS);
}
/**
* Index
*/
public function index() {
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (isset($_POST['logout'])) {
$this->validate_referer();
$this->clear_cookies();
$this->renderResponse(self::AUTH_OUT);
}
else {
return $this->authenticate();
}
}
if (isset($_COOKIE['pass_enabled'])) {
$this->renderResponse(self::AUTH_YES);
}
else {
$this->renderResponse(self::AUTH_NO);
}
}
/**
* Main
*/
public function run() {
$method = $_SERVER['REQUEST_METHOD'] === 'POST' ? $_POST : $_GET;
if (isset($method['action'])) {
$action = $method['action'];
}
else {
$action = 'index';
}
if (in_array($action, $this->actions)) {
if (isset($method['xhr'])) {
/*
if (isset($_SERVER['HTTP_ORIGIN']) && preg_match('/^https:\/\/sys\.(4chan|4channel)\.org$/', $_SERVER['HTTP_ORIGIN'])) {
header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
header('Access-Control-Allow-Methods: OPTIONS, POST');
header('Access-Control-Allow-Credentials: true');
}
*/
$this->is_xhr = true;
}
$this->$action();
}
else {
$this->error('Bad request');
}
}
}
$ctrl = new App();
$ctrl->run();