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, $ttl, $secure = false, $http_only = false) { $name = rawurlencode($name); $value = rawurlencode($value); $domain = '.' . THIS_DOMAIN; $flags = array(); if ($secure) { $flags[] = 'Secure'; } if ($http_only) { $flags[] = 'HttpOnly'; } if (!empty($flags)) { $flags = '; ' . implode('; ', $flags); } else { $flags = ''; } if ($ttl !== 0) { $max_age = " Max-Age=$ttl;"; } else { $max_age = ''; } header("Set-Cookie: $name=$value; Path=/;$max_age Domain=$domain; SameSite=None$flags", false); } private function clear_cookies() { $cookie_time = -3600; $this->set_cookie('pass_id', '', $cookie_time, true, true); $this->set_cookie('pass_enabled', '', $cookie_time, true); } 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 = 31556900; } else { $cookie_time = 86400; } $this->set_cookie('pass_id', "$id.$hashed_pass_session", $cookie_time, true, true); $this->set_cookie('pass_enabled', '1', $cookie_time, true); $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();