BioTorrents.de’s version of Gazelle
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.php 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. <?php
  2. #declare(strict_types=1);
  3. # Unsure if require_once is needed here
  4. require_once 'classes/env.class.php';
  5. $ENV = ENV::go();
  6. /*-- todo ---------------------------//
  7. Add the JavaScript validation into the display page using the class
  8. //-----------------------------------*/
  9. // Allow users to reset their password while logged in
  10. if (!empty($LoggedUser['ID']) && $_REQUEST['act'] !== 'recover') {
  11. header('Location: index.php');
  12. error();
  13. }
  14. if ($ENV->BLOCK_OPERA_MINI && isset($_SERVER['HTTP_X_OPERAMINI_PHONE'])) {
  15. error('Opera Mini is banned. Please use another browser.');
  16. }
  17. // Check if IP is banned
  18. if (Tools::site_ban_ip($_SERVER['REMOTE_ADDR'])) {
  19. error('Your IP address has been banned.');
  20. }
  21. # Initialize
  22. require_once SERVER_ROOT.'/classes/twofa.class.php';
  23. require_once SERVER_ROOT.'/classes/u2f.class.php';
  24. require_once SERVER_ROOT.'/classes/validate.class.php';
  25. $Validate = new Validate;
  26. $TwoFA = new TwoFactorAuth($ENV->SITE_NAME);
  27. $U2F = new u2f\U2F('https://'.SITE_DOMAIN);
  28. # todo: Test strict equality very gently here
  29. if (array_key_exists('action', $_GET) && $_GET['action'] === 'disabled') {
  30. require('disabled.php');
  31. error();
  32. }
  33. if (isset($_REQUEST['act']) && $_REQUEST['act'] === 'recover') {
  34. // Recover password
  35. if (!empty($_REQUEST['key'])) {
  36. // User has entered a new password, use step 2
  37. $DB->query("
  38. SELECT
  39. m.`ID`,
  40. m.`Email`,
  41. i.`ResetExpires`
  42. FROM `users_main` AS m
  43. INNER JOIN `users_info` AS i ON i.`UserID` = m.`ID`
  44. WHERE i.`ResetKey` = ?
  45. AND i.`ResetKey` != ''", $_REQUEST['key']);
  46. list($UserID, $Email, $Country, $Expires) = $DB->next_record();
  47. if (!apcu_exists('DBKEY')) {
  48. error('Database not fully decrypted. Please wait for staff to fix this and try again later.');
  49. }
  50. $Email = Crypto::decrypt($Email);
  51. if ($UserID && strtotime($Expires) > time()) {
  52. // If the user has requested a password change, and his key has not expired
  53. $Validate->SetFields('password', '1', 'regex', 'You entered an invalid password. Any password at least 15 characters long is accepted, but a strong password is 8 characters or longer, contains at least 1 lowercase and uppercase letter, contains at least a number or symbol', array('regex' => '/(?=^.{6,}$).*$/'));
  54. $Validate->SetFields('verifypassword', '1', 'compare', 'Your passwords did not match.', array('comparefield' => 'password'));
  55. if (!empty($_REQUEST['password'])) {
  56. // If the user entered a password.
  57. // If not, $PassWasReset !== 1, success message hidden.
  58. $Err = $Validate->ValidateForm($_REQUEST);
  59. if ($Err == '') {
  60. // Form validates without error, set new secret and password.
  61. $DB->query(
  62. "
  63. UPDATE
  64. `users_main` AS m,
  65. `users_info` AS i
  66. SET
  67. m.`PassHash` = ?,
  68. i.`ResetKey` = '',
  69. m.`LastLogin` = NOW(),
  70. i.`ResetExpires` = NULL
  71. WHERE m.`ID` = ?
  72. AND i.`UserID` = m.`ID`",
  73. Users::make_sec_hash($_REQUEST['password']),
  74. $UserID
  75. );
  76. $DB->query("
  77. INSERT INTO `users_history_passwords`
  78. (`UserID`, `ChangerIP`, `ChangeTime`)
  79. VALUES
  80. (?, ?, NOW())", $UserID, Crypto::encrypt($_SERVER['REMOTE_ADDR']));
  81. $PassWasReset = true;
  82. $LoggedUser['ID'] = $UserID; // Set $LoggedUser['ID'] for logout_all_sessions() to work
  83. logout_all_sessions();
  84. }
  85. }
  86. // Either a form asking for them to enter the password
  87. // Or a success message if $PassWasReset is 1
  88. require('recover_step2.php');
  89. } else {
  90. // Either his key has expired, or he hasn't requested a pass change at all
  91. if (strtotime($Expires) < time() && $UserID) {
  92. // If his key has expired, clear all the reset information
  93. $DB->query("
  94. UPDATE users_info
  95. SET ResetKey = '',
  96. ResetExpires = NULL
  97. WHERE UserID = ?", $UserID);
  98. $_SESSION['reseterr'] = 'The link you were given has expired.'; // Error message to display on form
  99. }
  100. // Show him the first form (enter email address)
  101. header('Location: login.php?act=recover');
  102. }
  103. } // End step 2
  104. // User has not clicked the link in his email, use step 1
  105. else {
  106. # Check for DBKEY before handling user inputs
  107. if (!apcu_exists('DBKEY')) {
  108. $Err = 'Database not fully decrypted. Please wait for staff to fix this and try again';
  109. }
  110. if (!empty($_REQUEST['email'])) {
  111. // User has entered email and submitted form
  112. $Validate->SetFields('email', '1', 'email', 'You entered an invalid email address');
  113. $Err = $Validate->ValidateForm($_REQUEST);
  114. if (!$Err) {
  115. // Form validates correctly
  116. $DB->query("
  117. SELECT
  118. `Email`
  119. FROM
  120. `users_main`
  121. ");
  122. /**
  123. * Note that if any user has a blank email field,
  124. * the comparison will fail and the script will 500!
  125. */
  126. while (list($EncEmail) = $DB->next_record()) {
  127. if (trim(
  128. strtolower($_REQUEST['email'])
  129. ) === strtolower(Crypto::decrypt($EncEmail))) {
  130. break; // $EncEmail is now the encrypted form of the given email from the database
  131. }
  132. }
  133. $DB->query("
  134. SELECT
  135. `ID`,
  136. `Username`,
  137. `Email`
  138. FROM
  139. `users_main`
  140. WHERE
  141. `Email` = '$EncEmail'
  142. ");
  143. list($UserID, $Username, $Email) = $DB->next_record();
  144. $Email = Crypto::decrypt($Email);
  145. if ($UserID) {
  146. // Email exists in the database
  147. // Set ResetKey, send out email, and set $Sent to 1 to show success page
  148. Users::reset_password($UserID, $Username, $Email);
  149. $Sent = 1; // If $Sent is 1, recover_step1.php displays a success message
  150. //Log out all of the users current sessions
  151. $Cache->delete_value("user_info_$UserID");
  152. $Cache->delete_value("user_info_heavy_$UserID");
  153. $Cache->delete_value("user_stats_$UserID");
  154. $Cache->delete_value("enabled_$UserID");
  155. $DB->query("
  156. SELECT
  157. `SessionID`
  158. FROM
  159. `users_sessions`
  160. WHERE
  161. `UserID` = '$UserID'
  162. ");
  163. while (list($SessionID) = $DB->next_record()) {
  164. $Cache->delete_value("session_$UserID"."_$SessionID");
  165. }
  166. $DB->query("
  167. UPDATE
  168. `users_sessions`
  169. SET
  170. `Active` = 0
  171. WHERE
  172. `UserID` = '$UserID'
  173. AND `Active` = 1
  174. ");
  175. } else {
  176. $Err = 'There is no user with that email address.';
  177. }
  178. }
  179. } elseif (!empty($_SESSION['reseterr'])) {
  180. // User has not entered email address, and there is an error set in session data
  181. // This is typically because their key has expired.
  182. // Stick the error into $Err so recover_step1.php can take care of it
  183. $Err = $_SESSION['reseterr'];
  184. unset($_SESSION['reseterr']);
  185. }
  186. // Either a form for the user's email address, or a success message
  187. require('recover_step1.php');
  188. }
  189. } // End password recovery
  190. elseif (isset($_REQUEST['act']) && $_REQUEST['act'] === 'newlocation') {
  191. if (isset($_REQUEST['key'])) {
  192. if ($ASNCache = $Cache->get_value('new_location_'.$_REQUEST['key'])) {
  193. $Cache->cache_value('new_location_'.$ASNCache['UserID'].'_'.$ASNCache['ASN'], true);
  194. require('newlocation.php');
  195. error();
  196. } else {
  197. error(403);
  198. }
  199. } else {
  200. error(403);
  201. }
  202. } // End new location
  203. // Normal login
  204. else {
  205. $Validate->SetFields('username', true, 'regex', 'You did not enter a valid username', array('regex' => USERNAME_REGEX));
  206. $Validate->SetFields('password', '1', 'string', 'You entered an invalid password', array('minlength' => '15', 'maxlength' => '307200'));
  207. list($Attempts, $Banned) = $Cache->get_value('login_attempts_'.db_string($_SERVER['REMOTE_ADDR']));
  208. // Function to log a user's login attempt
  209. function log_attempt()
  210. {
  211. global $Cache, $Attempts;
  212. $Attempts = ($Attempts ?? 0) + 1;
  213. $Cache->cache_value('login_attempts_'.db_string($_SERVER['REMOTE_ADDR']), array($Attempts, ($Attempts > 5)), 60*60*$Attempts);
  214. $AllAttempts = $Cache->get_value('login_attempts');
  215. $AllAttempts[$_SERVER['REMOTE_ADDR']] = time()+(60*60*$Attempts);
  216. foreach ($AllAttempts as $IP => $Time) {
  217. if ($Time < time()) {
  218. unset($AllAttempts[$IP]);
  219. }
  220. }
  221. $Cache->cache_value('login_attempts', $AllAttempts, 0);
  222. }
  223. // If user has submitted form
  224. if (isset($_POST['username']) && !empty($_POST['username']) && isset($_POST['password']) && !empty($_POST['password'])) {
  225. if ($Banned) {
  226. header("Location: login.php");
  227. error();
  228. }
  229. $Err = $Validate->ValidateForm($_POST);
  230. if (!$Err) {
  231. // Passes preliminary validation (username and password "look right")
  232. $DB->query("
  233. SELECT
  234. ID,
  235. PermissionID,
  236. CustomPermissions,
  237. PassHash,
  238. TwoFactor,
  239. Enabled
  240. FROM users_main
  241. WHERE Username = ?
  242. AND Username != ''", $_POST['username']);
  243. list($UserID, $PermissionID, $CustomPermissions, $PassHash, $TwoFactor, $Enabled) = $DB->next_record(MYSQLI_NUM, array(2));
  244. if (!$Banned) {
  245. if ($UserID && Users::check_password($_POST['password'], $PassHash)) {
  246. // Update hash if better algorithm available
  247. if (password_needs_rehash($PassHash, PASSWORD_DEFAULT)) {
  248. $DB->query("
  249. UPDATE users_main
  250. SET PassHash = ?
  251. WHERE Username = ?", make_sec_hash($_POST['password']), $_POST['username']);
  252. }
  253. if (empty($TwoFactor) || $TwoFA->verifyCode($TwoFactor, $_POST['twofa'])) {
  254. # todo: Make sure the type is (int)
  255. if ($Enabled === '1') {
  256. // Check if the current login attempt is from a location previously logged in from
  257. if (apcu_exists('DBKEY')) {
  258. $DB->query("
  259. SELECT
  260. `IP`
  261. FROM
  262. `users_history_ips`
  263. WHERE
  264. `UserID` = '$UserID'
  265. ");
  266. $IPs = $DB->to_array(false, MYSQLI_NUM);
  267. $QueryParts = [];
  268. foreach ($IPs as $i => $IP) {
  269. $IPs[$i] = Crypto::decrypt($IP[0]);
  270. }
  271. $IPs = array_unique($IPs);
  272. if (count($IPs) > 0) { // Always allow first login
  273. foreach ($IPs as $IP) {
  274. $QueryParts[] = "(StartIP<=INET6_ATON('$IP') AND EndIP>=INET6_ATON('$IP'))";
  275. }
  276. $DB->query('SELECT ASN FROM geoip_asn WHERE '.implode(' OR ', $QueryParts));
  277. $PastASNs = array_column($DB->to_array(false, MYSQLI_NUM), 0);
  278. $DB->query("SELECT ASN FROM geoip_asn WHERE StartIP<=INET6_ATON('$_SERVER[REMOTE_ADDR]') AND EndIP>=INET6_ATON('$_SERVER[REMOTE_ADDR]')");
  279. list($CurrentASN) = $DB->next_record();
  280. // If FEATURE_ENFORCE_LOCATIONS is enabled, require users to confirm new logins
  281. if (!in_array($CurrentASN, $PastASNs) && $ENV->FEATURE_ENFORCE_LOCATIONS) {
  282. // Never logged in from this location before
  283. if ($Cache->get_value('new_location_'.$UserID.'_'.$CurrentASN) !== true) {
  284. $DB->query("
  285. SELECT
  286. `UserName`,
  287. `Email`
  288. FROM
  289. `users_main`
  290. WHERE
  291. `ID` = '$UserID'
  292. ");
  293. list($Username, $Email) = $DB->next_record();
  294. Users::auth_location($UserID, $Username, $CurrentASN, Crypto::decrypt($Email));
  295. require('newlocation.php');
  296. error();
  297. }
  298. }
  299. }
  300. }
  301. $U2FRegs = [];
  302. $DB->query("
  303. SELECT KeyHandle, PublicKey, Certificate, Counter, Valid
  304. FROM u2f
  305. WHERE UserID = ?", $UserID);
  306. // Needs to be an array of objects, so we can't use to_array()
  307. while (list($KeyHandle, $PublicKey, $Certificate, $Counter, $Valid) = $DB->next_record()) {
  308. $U2FRegs[] = (object)['keyHandle'=>$KeyHandle, 'publicKey'=>$PublicKey, 'certificate'=>$Certificate, 'counter'=>$Counter, 'valid'=>$Valid];
  309. }
  310. if (sizeof($U2FRegs) > 0) {
  311. // U2F is enabled for this account
  312. if (isset($_POST['u2f-request']) && isset($_POST['u2f-response'])) {
  313. // Data from the U2F login page is present. Verify it.
  314. try {
  315. $U2FReg = $U2F->doAuthenticate(json_decode($_POST['u2f-request']), $U2FRegs, json_decode($_POST['u2f-response']));
  316. if ($U2FReg->valid != '1') {
  317. error('Token disabled.');
  318. }
  319. $DB->query(
  320. "UPDATE u2f
  321. SET Counter = ?
  322. WHERE KeyHandle = ?
  323. AND UserID = ?",
  324. $U2FReg->counter,
  325. $U2FReg->keyHandle,
  326. $UserID
  327. );
  328. } catch (Exception $e) {
  329. $U2FErr = 'U2F key invalid. Error: '.($e->getMessage());
  330. if ($e->getMessage() == 'Token disabled.') {
  331. $U2FErr = 'This token was disabled due to suspected cloning. Contact staff for assistance';
  332. }
  333. if ($e->getMessage() == 'Counter too low.') {
  334. $BadHandle = json_decode($_POST['u2f-response'], true)['keyHandle'];
  335. $DB->query("UPDATE u2f
  336. SET Valid = '0'
  337. WHERE KeyHandle = ?
  338. AND UserID = ?", $BadHandle, $UserID);
  339. $U2FErr = 'U2F counter too low. This token has been disabled due to suspected cloning. Contact staff for assistance';
  340. }
  341. }
  342. } else {
  343. // Data from the U2F login page is not present. Go there
  344. require('u2f.php');
  345. error();
  346. }
  347. }
  348. if (sizeof($U2FRegs) == 0 || !isset($U2FErr)) {
  349. $SessionID = Users::make_secret(64);
  350. setcookie('session', $SessionID, (time()+60*60*24*365), '/', '', true, true);
  351. setcookie('userid', $UserID, (time()+60*60*24*365), '/', '', true, true);
  352. // Because we <3 our staff
  353. $Permissions = Permissions::get_permissions($PermissionID);
  354. $CustomPermissions = unserialize($CustomPermissions);
  355. if (isset($Permissions['Permissions']['site_disable_ip_history'])
  356. || isset($CustomPermissions['site_disable_ip_history'])
  357. ) {
  358. $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
  359. }
  360. $DB->query("
  361. INSERT INTO users_sessions
  362. (UserID, SessionID, KeepLogged, Browser, OperatingSystem, IP, LastUpdate, FullUA)
  363. VALUES
  364. ('$UserID', '".db_string($SessionID)."', '1', '$Browser', '$OperatingSystem', '".db_string(apcu_exists('DBKEY')?Crypto::encrypt($_SERVER['REMOTE_ADDR']):'0.0.0.0')."', NOW(), '".db_string($_SERVER['HTTP_USER_AGENT'])."')");
  365. $Cache->begin_transaction("users_sessions_$UserID");
  366. $Cache->insert_front($SessionID, [
  367. 'SessionID' => $SessionID,
  368. 'Browser' => $Browser,
  369. 'OperatingSystem' => $OperatingSystem,
  370. 'IP' => (apcu_exists('DBKEY')?Crypto::encrypt($_SERVER['REMOTE_ADDR']):'0.0.0.0'),
  371. 'LastUpdate' => sqltime()
  372. ]);
  373. $Cache->commit_transaction(0);
  374. $Sql = "
  375. UPDATE users_main
  376. SET
  377. LastLogin = NOW(),
  378. LastAccess = NOW()
  379. WHERE ID = '".db_string($UserID)."'";
  380. $DB->query($Sql);
  381. if (!empty($_COOKIE['redirect'])) {
  382. $URL = $_COOKIE['redirect'];
  383. setcookie('redirect', '', time() - 60 * 60 * 24, '/', '', false);
  384. header("Location: $URL");
  385. error();
  386. } else {
  387. header('Location: index.php');
  388. error();
  389. }
  390. } else {
  391. log_attempt();
  392. $Err = $U2FErr;
  393. setcookie('keeplogged', '', time() + 60 * 60 * 24 * 365, '/', '', false);
  394. }
  395. } else {
  396. log_attempt();
  397. if ($Enabled == 2) {
  398. // Save the username in a cookie for the disabled page
  399. setcookie('username', db_string($_POST['username']), time() + 60 * 60, '/', '', false);
  400. header('Location: login.php?action=disabled');
  401. # todo: Make sure the type is (int)
  402. } elseif ($Enabled === '0') {
  403. $Err = 'Your account has not been confirmed. Please check your email, including the spam folder';
  404. }
  405. setcookie('keeplogged', '', time() + 60 * 60 * 24 * 365, '/', '', false);
  406. }
  407. } else {
  408. log_attempt();
  409. $Err = 'Two-factor authentication failed';
  410. setcookie('keeplogged', '', time() + 60 * 60 * 24 * 365, '/', '', false);
  411. }
  412. } else {
  413. log_attempt();
  414. $Err = 'Your username or password was incorrect';
  415. setcookie('keeplogged', '', time() + 60 * 60 * 24 * 365, '/', '', false);
  416. }
  417. } else {
  418. log_attempt();
  419. setcookie('keeplogged', '', time() + 60 * 60 * 24 * 365, '/', '', false);
  420. }
  421. } else {
  422. log_attempt();
  423. setcookie('keeplogged', '', time() + 60 * 60 * 24 * 365, '/', '', false);
  424. }
  425. }
  426. require('sections/login/login.php');
  427. }