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.

script_start.php 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. <?php
  2. #declare(strict_types=1);
  3. # https://www.php.net/manual/en/language.oop5.autoload.php
  4. require_once 'config.php';
  5. require_once 'security.class.php';
  6. # Initialize
  7. $ENV = ENV::go();
  8. $Security = new Security();
  9. $Security->SetupPitfalls();
  10. /*-- Script Start Class --------------------------------*/
  11. /*------------------------------------------------------*/
  12. /* This isnt really a class but a way to tie other */
  13. /* classes and functions used all over the site to the */
  14. /* page currently being displayed. */
  15. /*------------------------------------------------------*/
  16. /* The code that includes the main php files and */
  17. /* generates the page are at the bottom. */
  18. /*------------------------------------------------------*/
  19. /********************************************************/
  20. require SERVER_ROOT.'/classes/proxies.class.php';
  21. // Get the user's actual IP address if they're proxied.
  22. // Or if cloudflare is used
  23. if (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) {
  24. $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_CF_CONNECTING_IP'];
  25. }
  26. if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])
  27. && proxyCheck($_SERVER['REMOTE_ADDR'])
  28. && filter_var(
  29. $_SERVER['HTTP_X_FORWARDED_FOR'],
  30. FILTER_VALIDATE_IP,
  31. FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
  32. )) {
  33. $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
  34. }
  35. if (!isset($argv) && !empty($_SERVER['HTTP_HOST'])) {
  36. // Skip this block if running from cli or if the browser is old and shitty
  37. // This should really be done in nginx config
  38. // todo: Remove
  39. if ($_SERVER['HTTP_HOST'] == 'www.'.SITE_DOMAIN) {
  40. header('Location: https://'.SITE_DOMAIN.$_SERVER['REQUEST_URI']);
  41. error();
  42. }
  43. }
  44. $ScriptStartTime = microtime(true); // To track how long a page takes to create
  45. if (!defined('PHP_WINDOWS_VERSION_MAJOR')) {
  46. $RUsage = getrusage();
  47. $CPUTimeStart = $RUsage['ru_utime.tv_sec'] * 1000000 + $RUsage['ru_utime.tv_usec'];
  48. }
  49. ob_start(); // Start a buffer, mainly in case there is a mysql error
  50. require SERVER_ROOT.'/classes/debug.class.php'; // Require the debug class
  51. require SERVER_ROOT.'/classes/mysql.class.php'; // Require the database wrapper
  52. require SERVER_ROOT.'/classes/cache.class.php'; // Require the caching class
  53. require SERVER_ROOT.'/classes/time.class.php'; // Require the time class
  54. require SERVER_ROOT.'/classes/paranoia.class.php'; // Require the paranoia check_paranoia function
  55. require SERVER_ROOT.'/classes/util.php';
  56. $Debug = new DEBUG;
  57. $Debug->handle_errors();
  58. $Debug->set_flag('Debug constructed');
  59. $DB = new DB_MYSQL;
  60. $Cache = new Cache($ENV->getPriv('MEMCACHED_SERVERS'));
  61. // Autoload classes.
  62. require SERVER_ROOT.'/classes/autoload.php';
  63. // Note: G::initialize is called twice.
  64. // This is necessary as the code inbetween (initialization of $LoggedUser) makes use of G::$DB and G::$Cache.
  65. // todo: Remove one of the calls once we're moving everything into that class
  66. G::initialize();
  67. // Begin browser identification
  68. $Browser = UserAgent::browser($_SERVER['HTTP_USER_AGENT']);
  69. $OperatingSystem = UserAgent::operating_system($_SERVER['HTTP_USER_AGENT']);
  70. $Debug->set_flag('start user handling');
  71. // Get classes
  72. // todo: Remove these globals, replace by calls into Users
  73. list($Classes, $ClassLevels) = Users::get_classes();
  74. //-- Load user information
  75. // User info is broken up into many sections
  76. // Heavy - Things that the site never has to look at if the user isn't logged in (as opposed to things like the class, donor status, etc)
  77. // Light - Things that appear in format_user
  78. // Stats - Uploaded and downloaded - can be updated by a script if you want super speed
  79. // Session data - Information about the specific session
  80. // Enabled - if the user's enabled or not
  81. // Permissions
  82. /**
  83. * JSON API token support
  84. * @see https://github.com/OPSnet/Gazelle/commit/7c208fc4c396a16c77289ef886d0015db65f2af1#diff-2ea09cbf36b1d20fec7a6d7fc50780723b9f804c4e857003aa9a9c359dc9fd49
  85. */
  86. // Set the document we are loading
  87. $Document = basename(parse_url($_SERVER['SCRIPT_NAME'], PHP_URL_PATH), '.php');
  88. $LoggedUser = [];
  89. $SessionID = false;
  90. $FullToken = null;
  91. // Only allow using the Authorization header for ajax endpoint
  92. if (!empty($_SERVER['HTTP_AUTHORIZATION']) && $Document === 'ajax') {
  93. # Banned IP address
  94. if (IPv4::isBanned($_SERVER['REMOTE_ADDR'])) {
  95. header('Content-Type: application/json');
  96. json_die('failure', 'your ip address has been banned');
  97. }
  98. # Invalid auth header type
  99. # Bearer is correct according to RFC 6750
  100. # https://tools.ietf.org/html/rfc6750
  101. $AuthorizationHeader = explode(" ", (string) $_SERVER['HTTP_AUTHORIZATION']);
  102. if (count($AuthorizationHeader) === 2) {
  103. if ($AuthorizationHeader[0] !== 'Bearer') {
  104. header('Content-Type: application/json');
  105. json_die('failure', 'authorization type must be Bearer');
  106. }
  107. $FullToken = $AuthorizationHeader[1];
  108. } else {
  109. header('Content-Type: application/json');
  110. json_die('failure', 'authorization type must be Bearer');
  111. }
  112. $Revoked = 1;
  113. $UserID = (int) substr(Crypto::decrypt(base64UrlDecode($FullToken), $ENV->getPriv('ENCKEY')), 32);
  114. if (!empty($UserID)) {
  115. [$LoggedUser['ID'], $Revoked] =
  116. G::$DB->row("
  117. SELECT
  118. `UserID`,
  119. `Revoked`
  120. FROM
  121. `api_user_tokens`
  122. WHERE
  123. `UserID` = '$UserID'
  124. ");
  125. # AND `Token` = '$FullToken'
  126. } else {
  127. header('Content-Type: application/json');
  128. json_die('failure', 'invalid token format');
  129. }
  130. # No user or revoked API token
  131. if (empty($LoggedUser['ID']) || $Revoked === 1) {
  132. log_token_attempt(G::$DB);
  133. header('Content-Type: application/json');
  134. json_die('failure', 'token user mismatch');
  135. }
  136. # Checks if a user exists
  137. if (isset($LoggedUser['ID'])) {
  138. #$UserID = (int) $LoggedUser['ID'];
  139. #$Session = new Gazelle\Session($LoggedUser['ID']);
  140. # User doesn't own that token
  141. if (!is_null($FullToken) && !Users::hasApiToken($UserID, $FullToken)) {
  142. log_token_attempt(G::$DB, $LoggedUser['ID']);
  143. header('Content-type: application/json');
  144. json_die('failure', 'token revoked');
  145. }
  146. # User is disabled
  147. if (Users::isDisabled($UserID)) {
  148. if (is_null($FullToken)) {
  149. logout($LoggedUser['ID'], $SessionID);
  150. } else {
  151. log_token_attempt(G::$DB, $LoggedUser['ID']);
  152. header('Content-type: application/json');
  153. json_die('failure', 'user disabled');
  154. }
  155. }
  156. }
  157. }
  158. /*
  159. # OPS pleasantly rewrote session handling
  160. $UserSessions = [];
  161. if (isset($_COOKIE['session'])) {
  162. $LoginCookie = Crypto::decrypt($_COOKIE['session'], $ENV->getPriv('ENCKEY'));
  163. if ($LoginCookie !== false) {
  164. [$SessionID, $LoggedUser['ID']] = explode('|~|', Crypto::decrypt($LoginCookie, $ENV->getPriv('ENCKEY')));
  165. $LoggedUser['ID'] = (int)$LoggedUser['ID'];
  166. if (!$LoggedUser['ID'] || !$SessionID) {
  167. logout($LoggedUser['ID'], $SessionID);
  168. }
  169. $Session = new Gazelle\Session($LoggedUser['ID']);
  170. $UserSessions = $Session->sessions();
  171. if (!array_key_exists($SessionID, $UserSessions)) {
  172. logout($LoggedUser['ID'], $SessionID);
  173. }
  174. }
  175. }
  176. */
  177. # End OPS API token additions
  178. /**
  179. * Session handling and cookies
  180. */
  181. if (isset($_COOKIE['session']) && isset($_COOKIE['userid'])) {
  182. $SessionID = $_COOKIE['session'];
  183. $LoggedUser['ID'] = (int) $_COOKIE['userid'];
  184. $UserID = $LoggedUser['ID']; // todo: UserID should not be LoggedUser
  185. if (!$LoggedUser['ID'] || !$SessionID) {
  186. logout();
  187. }
  188. $UserSessions = $Cache->get_value("users_sessions_$UserID");
  189. if (!is_array($UserSessions)) {
  190. $DB->query(
  191. "
  192. SELECT
  193. SessionID,
  194. Browser,
  195. OperatingSystem,
  196. IP,
  197. LastUpdate
  198. FROM users_sessions
  199. WHERE UserID = '$UserID'
  200. AND Active = 1
  201. ORDER BY LastUpdate DESC"
  202. );
  203. $UserSessions = $DB->to_array('SessionID', MYSQLI_ASSOC);
  204. $Cache->cache_value("users_sessions_$UserID", $UserSessions, 0);
  205. }
  206. if (!array_key_exists($SessionID, $UserSessions)) {
  207. logout();
  208. }
  209. // Check if user is enabled
  210. $Enabled = $Cache->get_value('enabled_'.$LoggedUser['ID']);
  211. if ($Enabled === false) {
  212. $DB->query("
  213. SELECT Enabled
  214. FROM users_main
  215. WHERE ID = '$LoggedUser[ID]'");
  216. list($Enabled) = $DB->next_record();
  217. $Cache->cache_value('enabled_'.$LoggedUser['ID'], $Enabled, 0);
  218. }
  219. # todo: Check strict equality
  220. if ($Enabled == 2) {
  221. logout();
  222. }
  223. // Up/Down stats
  224. $UserStats = $Cache->get_value('user_stats_'.$LoggedUser['ID']);
  225. if (!is_array($UserStats)) {
  226. $DB->query("
  227. SELECT Uploaded AS BytesUploaded, Downloaded AS BytesDownloaded, RequiredRatio
  228. FROM users_main
  229. WHERE ID = '$LoggedUser[ID]'");
  230. $UserStats = $DB->next_record(MYSQLI_ASSOC);
  231. $Cache->cache_value('user_stats_'.$LoggedUser['ID'], $UserStats, 3600);
  232. }
  233. // Get info such as username
  234. $LightInfo = Users::user_info($LoggedUser['ID']);
  235. $HeavyInfo = Users::user_heavy_info($LoggedUser['ID']);
  236. /**
  237. * OPS API tokens
  238. * @see https://github.com/OPSnet/Gazelle/commit/7c208fc4c396a16c77289ef886d0015db65f2af1#diff-2ea09cbf36b1d20fec7a6d7fc50780723b9f804c4e857003aa9a9c359dc9fd49
  239. */
  240. // TODO: These globals need to die, and just use $LoggedUser
  241. // TODO: And then instantiate $LoggedUser from Gazelle\Session when needed
  242. if (empty($LightInfo['Username'])) { // Ghost
  243. logout($LoggedUser['ID'], $SessionID);
  244. if (!is_null($FullToken)) {
  245. #$UserID->flushCache();
  246. log_token_attempt(G::$DB, $LoggedUser['ID']);
  247. header('Content-type: application/json');
  248. json_die('error', 'invalid token');
  249. } else {
  250. logout($LoggedUser['ID'], $SessionID);
  251. }
  252. }
  253. # End OPS API token additions
  254. // Create LoggedUser array
  255. $LoggedUser = array_merge($HeavyInfo, $LightInfo, $UserStats);
  256. $LoggedUser['RSS_Auth'] = md5($LoggedUser['ID'] . $ENV->getPriv('RSS_HASH') . $LoggedUser['torrent_pass']);
  257. // $LoggedUser['RatioWatch'] as a bool to disable things for users on Ratio Watch
  258. $LoggedUser['RatioWatch'] = (
  259. $LoggedUser['RatioWatchEnds']
  260. && time() < strtotime($LoggedUser['RatioWatchEnds'])
  261. && ($LoggedUser['BytesDownloaded'] * $LoggedUser['RequiredRatio']) > $LoggedUser['BytesUploaded']
  262. );
  263. // Load in the permissions
  264. $LoggedUser['Permissions'] = Permissions::get_permissions_for_user($LoggedUser['ID'], $LoggedUser['CustomPermissions']);
  265. $LoggedUser['Permissions']['MaxCollages'] += Donations::get_personal_collages($LoggedUser['ID']);
  266. // Change necessary triggers in external components
  267. $Cache->CanClear = check_perms('admin_clear_cache');
  268. // Because we <3 our staff
  269. if (check_perms('site_disable_ip_history')) {
  270. $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
  271. }
  272. // Update LastUpdate every 10 minutes
  273. if (strtotime($UserSessions[$SessionID]['LastUpdate']) + 600 < time()) {
  274. $DB->query("
  275. UPDATE users_main
  276. SET LastAccess = NOW()
  277. WHERE ID = '$LoggedUser[ID]'
  278. ");
  279. $SessionQuery =
  280. "UPDATE users_sessions
  281. SET ";
  282. // Only update IP if we have an encryption key in memory
  283. if (apcu_exists('DBKEY')) {
  284. $SessionQuery .= "IP = '".Crypto::encrypt($_SERVER['REMOTE_ADDR'])."', ";
  285. }
  286. $SessionQuery .=
  287. "Browser = '$Browser',
  288. OperatingSystem = '$OperatingSystem',
  289. LastUpdate = NOW()
  290. WHERE UserID = '$LoggedUser[ID]'
  291. AND SessionID = '".db_string($SessionID)."'";
  292. $DB->query($SessionQuery);
  293. $Cache->begin_transaction("users_sessions_$UserID");
  294. $Cache->delete_row($SessionID);
  295. $UsersSessionCache = array(
  296. 'SessionID' => $SessionID,
  297. 'Browser' => $Browser,
  298. 'OperatingSystem' => $OperatingSystem,
  299. 'IP' => (apcu_exists('DBKEY') ? Crypto::encrypt($_SERVER['REMOTE_ADDR']) : $UserSessions[$SessionID]['IP']),
  300. 'LastUpdate' => sqltime() );
  301. $Cache->insert_front($SessionID, $UsersSessionCache);
  302. $Cache->commit_transaction(0);
  303. }
  304. // Notifications
  305. if (isset($LoggedUser['Permissions']['site_torrents_notify'])) {
  306. $LoggedUser['Notify'] = $Cache->get_value('notify_filters_'.$LoggedUser['ID']);
  307. if (!is_array($LoggedUser['Notify'])) {
  308. $DB->query("
  309. SELECT ID, Label
  310. FROM users_notify_filters
  311. WHERE UserID = '$LoggedUser[ID]'");
  312. $LoggedUser['Notify'] = $DB->to_array('ID');
  313. $Cache->cache_value('notify_filters_'.$LoggedUser['ID'], $LoggedUser['Notify'], 2592000);
  314. }
  315. }
  316. // We've never had to disable the wiki privs of anyone.
  317. if ($LoggedUser['DisableWiki']) {
  318. unset($LoggedUser['Permissions']['site_edit_wiki']);
  319. }
  320. // IP changed
  321. if (apcu_exists('DBKEY') && Crypto::decrypt($LoggedUser['IP']) != $_SERVER['REMOTE_ADDR'] && !check_perms('site_disable_ip_history')) {
  322. if (Tools::site_ban_ip($_SERVER['REMOTE_ADDR'])) {
  323. error('Your IP address has been banned.');
  324. }
  325. $CurIP = db_string($LoggedUser['IP']);
  326. $NewIP = db_string($_SERVER['REMOTE_ADDR']);
  327. $DB->query("
  328. SELECT IP
  329. FROM users_history_ips
  330. WHERE EndTime IS NULL
  331. AND UserID = '$LoggedUser[ID]'");
  332. while (list($EncIP) = $DB->next_record()) {
  333. if (Crypto::decrypt($EncIP) == $CurIP) {
  334. $CurIP = $EncIP;
  335. // CurIP is now the encrypted IP that was already in the database (for matching)
  336. break;
  337. }
  338. }
  339. $DB->query("
  340. UPDATE users_history_ips
  341. SET EndTime = NOW()
  342. WHERE EndTime IS NULL
  343. AND UserID = '$LoggedUser[ID]'
  344. AND IP = '$CurIP'");
  345. $DB->query("
  346. INSERT IGNORE INTO users_history_ips
  347. (UserID, IP, StartTime)
  348. VALUES
  349. ('$LoggedUser[ID]', '".Crypto::encrypt($NewIP)."', NOW())");
  350. $Cache->begin_transaction('user_info_heavy_'.$LoggedUser['ID']);
  351. $Cache->update_row(false, array('IP' => Crypto::encrypt($_SERVER['REMOTE_ADDR'])));
  352. $Cache->commit_transaction(0);
  353. }
  354. // Get stylesheets
  355. $Stylesheets = $Cache->get_value('stylesheets');
  356. if (!is_array($Stylesheets)) {
  357. $DB->query('
  358. SELECT
  359. ID,
  360. LOWER(REPLACE(Name, " ", "_")) AS Name,
  361. Name AS ProperName,
  362. LOWER(REPLACE(Additions, " ", "_")) AS Additions,
  363. Additions AS ProperAdditions
  364. FROM stylesheets');
  365. $Stylesheets = $DB->to_array('ID', MYSQLI_BOTH);
  366. $Cache->cache_value('stylesheets', $Stylesheets, 0);
  367. }
  368. // todo: Clean up this messy solution
  369. $LoggedUser['StyleName'] = $Stylesheets[$LoggedUser['StyleID']]['Name'];
  370. if (empty($LoggedUser['Username'])) {
  371. logout(); // Ghost
  372. }
  373. }
  374. G::initialize();
  375. $Debug->set_flag('end user handling');
  376. $Debug->set_flag('start function definitions');
  377. /**
  378. * Log out the current session
  379. */
  380. function logout()
  381. {
  382. global $SessionID;
  383. setcookie('session', '', time() - 60 * 60 * 24 * 365, '/', '', false);
  384. setcookie('userid', '', time() - 60 * 60 * 24 * 365, '/', '', false);
  385. setcookie('keeplogged', '', time() - 60 * 60 * 24 * 365, '/', '', false);
  386. if ($SessionID) {
  387. G::$DB->query("
  388. DELETE FROM users_sessions
  389. WHERE UserID = '" . G::$LoggedUser['ID'] . "'
  390. AND SessionID = '".db_string($SessionID)."'");
  391. G::$Cache->begin_transaction('users_sessions_' . G::$LoggedUser['ID']);
  392. G::$Cache->delete_row($SessionID);
  393. G::$Cache->commit_transaction(0);
  394. }
  395. G::$Cache->delete_value('user_info_' . G::$LoggedUser['ID']);
  396. G::$Cache->delete_value('user_stats_' . G::$LoggedUser['ID']);
  397. G::$Cache->delete_value('user_info_heavy_' . G::$LoggedUser['ID']);
  398. header('Location: login.php');
  399. error();
  400. }
  401. function logout_all_sessions()
  402. {
  403. $UserID = G::$LoggedUser['ID'];
  404. G::$DB->query("
  405. DELETE FROM users_sessions
  406. WHERE UserID = '$UserID'");
  407. G::$Cache->delete_value('users_sessions_' . $UserID);
  408. logout();
  409. }
  410. function enforce_login()
  411. {
  412. global $SessionID;
  413. if (!$SessionID || !G::$LoggedUser) {
  414. setcookie('redirect', $_SERVER['REQUEST_URI'], time() + 60 * 30, '/', '', false);
  415. logout();
  416. }
  417. }
  418. /**
  419. * Make sure $_GET['auth'] is the same as the user's authorization key
  420. * Should be used for any user action that relies solely on GET.
  421. *
  422. * @param Are we using ajax?
  423. * @return authorisation status. Prints an error message to DEBUG_CHAN on IRC on failure.
  424. */
  425. function authorize($Ajax = false)
  426. {
  427. # Ugly workaround for API tokens
  428. if (!empty($_SERVER['HTTP_AUTHORIZATION']) && $Document === 'ajax') {
  429. return true;
  430. } else {
  431. if (empty($_REQUEST['auth']) || $_REQUEST['auth'] !== G::$LoggedUser['AuthKey']) {
  432. send_irc(DEBUG_CHAN, G::$LoggedUser['Username']." just failed authorize on ".$_SERVER['REQUEST_URI'].(!empty($_SERVER['HTTP_REFERER']) ? " coming from ".$_SERVER['HTTP_REFERER'] : ""));
  433. error('Invalid authorization key. Go back, refresh, and try again.', $NoHTML = true);
  434. return false;
  435. }
  436. }
  437. }
  438. $Debug->set_flag('ending function definitions');
  439. $Document = basename(parse_url($_SERVER['SCRIPT_FILENAME'], PHP_URL_PATH), '.php');
  440. if (!preg_match('/^[a-z0-9]+$/i', $Document)) {
  441. error(404);
  442. }
  443. $StripPostKeys = array_fill_keys(array('password', 'cur_pass', 'new_pass_1', 'new_pass_2', 'verifypassword', 'confirm_password', 'ChangePassword', 'Password'), true);
  444. $Cache->cache_value('php_' . getmypid(), array(
  445. 'start' => sqltime(),
  446. 'document' => $Document,
  447. 'query' => $_SERVER['QUERY_STRING'],
  448. 'get' => $_GET,
  449. 'post' => array_diff_key($_POST, $StripPostKeys)), 600);
  450. // Locked account constant
  451. define('STAFF_LOCKED', 1);
  452. $AllowedPages = ['staffpm', 'ajax', 'locked', 'logout', 'login'];
  453. if (isset(G::$LoggedUser['LockedAccount']) && !in_array($Document, $AllowedPages)) {
  454. require(SERVER_ROOT . '/sections/locked/index.php');
  455. } else {
  456. require(SERVER_ROOT . '/sections/' . $Document . '/index.php');
  457. }
  458. $Debug->set_flag('completed module execution');
  459. // Flush to user
  460. ob_end_flush();
  461. $Debug->set_flag('set headers and send to user');
  462. // Attribute profiling
  463. $Debug->profile();