/ auth-test.php
auth-test.php
1 <?php 2 die(); 3 define('IS_4CHANNEL', preg_match('/(^|\.)4channel.org$/', $_SERVER['HTTP_HOST'])); 4 5 if (IS_4CHANNEL) { 6 define('THIS_DOMAIN', '4channel.org'); 7 define('OTHER_DOMAIN', '4chan.org'); 8 } 9 else { 10 define('THIS_DOMAIN', '4chan.org'); 11 define('OTHER_DOMAIN', '4channel.org'); 12 } 13 14 define('PASS_TIMEOUT', 900); // 15 minutes 15 define('LOGIN_FAIL_HOURLY', 5); 16 17 require_once 'lib/db.php'; 18 require_once 'lib/geoip2.php'; 19 20 class App { 21 protected 22 // Routes 23 $actions = array( 24 'index' 25 ), 26 27 $is_xhr = false 28 ; 29 30 const VIEW_TPL = 'views/pass_auth.tpl.php'; 31 32 const PASS_TABLE = 'pass_users'; 33 34 const 35 AUTH_NO = 0, 36 AUTH_SUCCESS = 1, 37 AUTH_YES = 2, 38 AUTH_ERROR = -1, 39 AUTH_OUT = 4 40 ; 41 42 const 43 ERR_BAD_REQUEST = 'Bad Request.', 44 ERR_GENERIC = 'Internal Server Error (%s)', 45 ERR_FLOOD = 'You have to wait a while before attempting this again.', 46 ERR_EMPTY_FIELD = 'You have left one or more fields blank.', 47 ERR_TOKEN_LEN = 'Your Token must be exactly 10 characters.', 48 ERR_DB = 'We are currently having database issues. Please try again later.', 49 ERR_BAD_AUTH = 'Incorrect Token or PIN.', 50 ERR_IN_USE = 'This Pass is already in use by another IP. Please wait %s and re-authorize by visiting this page again to change IPs.', 51 ERR_EXPIRED = 'This Pass has expired. Please visit <a href="https://www.4chan.org/pass.php?renew=%s">this page</a> to renew it.', // status 1 52 ERR_REFUNDED = 'This Pass has been refunded and disabled. You cannot use it anymore.', // status 2 53 ERR_DISPUTED = 'This Pass has a disputed payment. You cannot use it until the dispute is resolved.', // status 3 54 ERR_REVOKED_SPAM = 'This Pass has been revoked due to spamming, which is a violation of the <a href="https://www.4chan.org/pass#termsofuse">Terms of Use</a>.', // status 4 55 ERR_REVOKED_ILLEGAL = 'This Pass has been revoked due to illegal content being posted, which is a violaton of the <a href="https://www.4chan.org/pass#termsofuse">Terms of Use</a>.' // status 5 56 ; 57 58 private function error($msg) { 59 $this->renderResponse(self::AUTH_ERROR, $msg); 60 } 61 62 private function renderResponse($status, $msg = null) { 63 if ($this->is_xhr) { 64 header('Content-type: application/json'); 65 echo json_encode(array('status' => $status, 'message' => $msg)); 66 } 67 else { 68 $this->auth_status = $status; 69 $this->message = $msg; 70 require_once(self::VIEW_TPL); 71 } 72 die(); 73 } 74 75 private function pretty_duration($sec) { 76 $duration = ''; 77 78 $hours = (int)($sec / 3600); 79 $minutes = (int)($sec / 60); 80 81 if ($hours) { 82 $duration .= str_pad($hours, 2, '0', STR_PAD_LEFT) . ' hour'; 83 84 if ($hours != 1) { 85 $duration .= 's'; 86 } 87 88 $duration .= ' '; 89 } 90 91 if ($minutes) { 92 $minutes = (int)(($sec / 60) % 60); 93 94 $duration .= str_pad($minutes, 2, '0', STR_PAD_LEFT). ' minute'; 95 96 if ($minutes != 1) { 97 $duration .= 's'; 98 } 99 } 100 101 $seconds = intval($sec % 60); 102 103 return $duration; 104 } 105 106 private function get_csrf_token() { 107 return bin2hex(openssl_random_pseudo_bytes(16)); 108 } 109 110 private function validate_referer() { 111 if (!isset($_SERVER['HTTP_REFERER']) || $_SERVER['HTTP_REFERER'] === '') { 112 return; 113 } 114 115 if (!preg_match('/^https:\/\/sys\.(4chan|4channel)\.org(\/|$)/', $_SERVER['HTTP_REFERER'])) { 116 $this->error(self::ERR_BAD_REQUEST); 117 } 118 } 119 120 private function validate_csrf() { 121 if (!isset($_COOKIE['csrf']) || !isset($_POST['csrf']) 122 || $_COOKIE['csrf'] === '' || $_POST['csrf'] === '') { 123 $this->error(self::ERR_BAD_REQUEST); 124 } 125 126 if ($_COOKIE['csrf'] !== $_POST['csrf']) { 127 $this->error(self::ERR_BAD_REQUEST); 128 } 129 } 130 131 private function validate_auth_flood($long_ip) { 132 if (!$long_ip) { 133 return; 134 } 135 136 $query = "SELECT COUNT(ip) FROM user_actions WHERE ip = $long_ip AND action = 'fail_pass_auth' AND time >= DATE_SUB(NOW(), INTERVAL 1 HOUR)"; 137 138 $res = mysql_global_call($query); 139 140 if (!$res) { 141 return; 142 } 143 144 $count = (int)mysql_fetch_row($res)[0]; 145 146 if ($count >= LOGIN_FAIL_HOURLY) { 147 $this->error(self::ERR_FLOOD); 148 } 149 } 150 151 private function register_auth_failure($long_ip) { 152 if (!$long_ip) { 153 return; 154 } 155 156 $query = "INSERT INTO user_actions (ip, board, action, time) VALUES(%d, '', 'fail_pass_auth', NOW())"; 157 $res = mysql_global_call($query, $long_ip); 158 } 159 160 private function convert_new_pass_status($user_hash, $hashed_pin) { 161 $table = self::PASS_TABLE; 162 163 $query = "UPDATE $table SET pin = '%s', status = 0 WHERE user_hash = '%s' AND status = 6 LIMIT 1"; 164 165 mysql_global_call($query, $hashed_pin, $user_hash); 166 167 $this->set_cookie('pass_email', '', -1); 168 } 169 170 private function convert_delayed_pass_status($user_hash, $hashed_pin) { 171 $table = self::PASS_TABLE; 172 173 $query = "UPDATE $table SET pin = '%s', status = 0, expiration_date = NOW() + INTERVAL 1 YEAR WHERE user_hash = '%s' AND status = 7 LIMIT 1"; 174 175 mysql_global_call($query, $hashed_pin, $user_hash); 176 } 177 178 private function set_cookie($name, $value, $expire, $secure = false, $http_only = false) { 179 setcookie($name, $value, $expire, '/', '.' . THIS_DOMAIN, $secure, $http_only); 180 } 181 182 private function clear_cookies() { 183 $cookie_time = $_SERVER['REQUEST_TIME'] - 3600; 184 $this->set_cookie('pass_id', null, $cookie_time, true, true); 185 $this->set_cookie('pass_enabled', null, $cookie_time); 186 } 187 188 private function get_random_base64bytes($length = 64) { 189 $data = openssl_random_pseudo_bytes($length); 190 191 return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); 192 } 193 194 private function get_salt() { 195 $salt = file_get_contents('/www/keys/2014_admin.salt'); 196 197 if (!$salt) { 198 $this->error(sprintf(self::ERR_GENERIC, 'gs')); 199 } 200 201 return $salt; 202 } 203 204 /** 205 * Login 206 */ 207 private function authenticate() { 208 $this->validate_referer(); 209 210 $table = self::PASS_TABLE; 211 212 $time_now = time(); 213 214 // Token 215 if (!isset($_POST['id']) || $_POST['id'] === '') { 216 $this->error(self::ERR_EMPTY_FIELD); 217 } 218 219 if (strlen($_POST['id']) != 10) { 220 $this->error(self::ERR_TOKEN_LEN); 221 } 222 223 $id = $_POST['id']; 224 225 // Pin 226 if (!isset($_POST['pin']) || $_POST['pin'] === '') { 227 $this->error(self::ERR_EMPTY_FIELD); 228 } 229 230 $pin = $_POST['pin']; 231 232 // --- 233 234 $ip = $_SERVER['REMOTE_ADDR']; 235 $long_ip = ip2long($ip); 236 237 $this->validate_auth_flood($long_ip); 238 239 // --- 240 241 $plain_pin = $pin; 242 $pin = crypt($pin, substr($id, 4, 9)); 243 244 $query = "SELECT * FROM $table WHERE user_hash = '%s' AND (pin = '%s' OR pin = '%s') LIMIT 1"; 245 246 $res = mysql_global_call($query, $id, $pin, $plain_pin); 247 248 if (!$res) { 249 $this->error(self::ERR_DB); 250 } 251 252 if (mysql_num_rows($res) !== 1) { 253 $this->register_auth_failure($long_ip); 254 $this->error(self::ERR_BAD_AUTH); 255 } 256 257 $pass = mysql_fetch_assoc($res); 258 259 if (!$pass) { 260 $this->error(sprintf(self::ERR_GENERIC, 'mfa1')); 261 } 262 263 $last_used = strtotime($pass['last_used']); 264 265 $last_ip_mask = ip2long($pass['last_ip']) & (~65535); 266 267 $ip_mask = $long_ip & (~65535); 268 269 if ($last_ip_mask !== 0 && ($time_now - $last_used) < PASS_TIMEOUT && $last_ip_mask != $ip_mask) { 270 $remaining = $this->pretty_duration(PASS_TIMEOUT - ($time_now - $last_used)); 271 $this->error(sprintf(self::ERR_IN_USE, $remaining)); 272 } 273 274 switch ($pass['status']){ 275 case 0: 276 break; 277 278 case 1: 279 $this->clear_cookies(); 280 $this->error(sprintf(self::ERR_EXPIRED, $pass['pending_id'])); 281 break; 282 283 case 2: 284 $this->clear_cookies(); 285 $this->error(self::ERR_REFUNDED); 286 break; 287 288 case 3: 289 $this->clear_cookies(); 290 $this->error(self::ERR_DISPUTED); 291 break; 292 293 case 4: 294 $this->clear_cookies(); 295 $this->error(self::ERR_REVOKED_SPAM); 296 break; 297 298 case 5: 299 $this->clear_cookies(); 300 $this->error(self::ERR_REVOKED_ILLEGAL); 301 break; 302 303 case 6: 304 $this->convert_new_pass_status($pass['user_hash'], $pin); 305 break; 306 307 case 7: 308 $this->convert_delayed_pass_status($pass['user_hash'], $pin); 309 break; 310 } 311 312 // Update country 313 $geo_data = GeoIP2::get_country($ip); 314 315 if ($geo_data && isset($geo_data['country_code'])) { 316 $country_code = mysql_real_escape_string($geo_data['country_code']); 317 } 318 else { 319 $country_code = 'XX'; 320 } 321 322 $update_country = ", last_country = '$country_code'"; 323 324 $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"; 325 326 mysql_global_call($query, $ip, $id, $ip); 327 328 // Update session id 329 if (!$pass['session_id']) { 330 $pass_session = $this->get_random_base64bytes(32); 331 332 if (!$pass_session) { 333 $this->error(sprintf(self::ERR_GENERIC, 'grb')); 334 } 335 336 $query = "UPDATE $table SET session_id = '$pass_session' WHERE user_hash = '%s' AND status = 0 LIMIT 1"; 337 338 mysql_global_call($query, $id); 339 } 340 else { 341 $pass_session = $pass['session_id']; 342 } 343 344 $admin_salt = $this->get_salt(); 345 346 $hashed_pass_session = substr(hash('sha256', $pass_session . $admin_salt), 0, 32); 347 348 if (!$hashed_pass_session) { 349 $this->error(sprintf(self::ERR_GENERIC, 'hps')); 350 } 351 352 if (isset($_POST['long_login'])) { 353 $cookie_time = $time_now + 31556900; 354 } 355 else { 356 $cookie_time = $time_now + 86400; 357 } 358 359 $this->set_cookie('pass_id', "$id.$hashed_pass_session", $cookie_time, true, true); 360 $this->set_cookie('pass_enabled', '1', $cookie_time); 361 362 $this->renderResponse(self::AUTH_SUCCESS); 363 } 364 365 /** 366 * Index 367 */ 368 public function index() { 369 if ($_SERVER['REQUEST_METHOD'] == 'POST') { 370 if (isset($_POST['logout'])) { 371 $this->validate_referer(); 372 $this->clear_cookies(); 373 $this->renderResponse(self::AUTH_OUT); 374 } 375 else { 376 return $this->authenticate(); 377 } 378 } 379 380 if (isset($_COOKIE['pass_enabled'])) { 381 $this->renderResponse(self::AUTH_YES); 382 } 383 else { 384 $this->renderResponse(self::AUTH_NO); 385 } 386 } 387 388 /** 389 * Main 390 */ 391 public function run() { 392 $method = $_SERVER['REQUEST_METHOD'] === 'POST' ? $_POST : $_GET; 393 394 if (isset($method['action'])) { 395 $action = $method['action']; 396 } 397 else { 398 $action = 'index'; 399 } 400 401 if (in_array($action, $this->actions)) { 402 if (isset($method['xhr'])) { 403 /* 404 if (isset($_SERVER['HTTP_ORIGIN']) && preg_match('/^https:\/\/sys\.(4chan|4channel)\.org$/', $_SERVER['HTTP_ORIGIN'])) { 405 header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']); 406 header('Access-Control-Allow-Methods: OPTIONS, POST'); 407 header('Access-Control-Allow-Credentials: true'); 408 } 409 */ 410 $this->is_xhr = true; 411 } 412 413 $this->$action(); 414 } 415 else { 416 $this->error('Bad request'); 417 } 418 } 419 } 420 421 $ctrl = new App(); 422 $ctrl->run();