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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. <?php
  2. #declare(strict_types=1);
  3. # Initialize
  4. require_once 'config.php';
  5. require_once 'security.class.php';
  6. $ENV = ENV::go();
  7. $Security = new Security();
  8. $Security->SetupPitfalls();
  9. /*-- Script Start Class --------------------------------*/
  10. /*------------------------------------------------------*/
  11. /* This isnt really a class but a way to tie other */
  12. /* classes and functions used all over the site to the */
  13. /* page currently being displayed. */
  14. /*------------------------------------------------------*/
  15. /* The code that includes the main php files and */
  16. /* generates the page are at the bottom. */
  17. /*------------------------------------------------------*/
  18. /********************************************************/
  19. require SERVER_ROOT.'/classes/proxies.class.php';
  20. // Get the user's actual IP address if they're proxied.
  21. // Or if cloudflare is used
  22. if (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) {
  23. $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_CF_CONNECTING_IP'];
  24. }
  25. if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])
  26. && proxyCheck($_SERVER['REMOTE_ADDR'])
  27. && filter_var(
  28. $_SERVER['HTTP_X_FORWARDED_FOR'],
  29. FILTER_VALIDATE_IP,
  30. FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
  31. )) {
  32. $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
  33. }
  34. if (!isset($argv) && !empty($_SERVER['HTTP_HOST'])) {
  35. // Skip this block if running from cli or if the browser is old and shitty
  36. // This should really be done in nginx config
  37. // todo: Remove
  38. if ($_SERVER['HTTP_HOST'] == 'www.'.SITE_DOMAIN) {
  39. header('Location: https://'.SITE_DOMAIN.$_SERVER['REQUEST_URI']);
  40. error();
  41. }
  42. }
  43. $ScriptStartTime = microtime(true); // To track how long a page takes to create
  44. if (!defined('PHP_WINDOWS_VERSION_MAJOR')) {
  45. $RUsage = getrusage();
  46. $CPUTimeStart = $RUsage['ru_utime.tv_sec'] * 1000000 + $RUsage['ru_utime.tv_usec'];
  47. }
  48. ob_start(); // Start a buffer, mainly in case there is a mysql error
  49. require_once SERVER_ROOT.'/classes/debug.class.php'; // Require the debug class
  50. require_once SERVER_ROOT.'/classes/mysql.class.php'; // Require the database wrapper
  51. require_once SERVER_ROOT.'/classes/cache.class.php'; // Require the caching class
  52. require_once SERVER_ROOT.'/classes/time.class.php'; // Require the time class
  53. require_once SERVER_ROOT.'/classes/paranoia.class.php'; // Require the paranoia check_paranoia function
  54. require_once SERVER_ROOT.'/classes/util.php';
  55. $Debug = new DEBUG;
  56. $Debug->handle_errors();
  57. $Debug->set_flag('Debug constructed');
  58. $DB = new DB_MYSQL;
  59. $Cache = new Cache($ENV->getPriv('MEMCACHED_SERVERS'));
  60. // Autoload classes.
  61. require_once SERVER_ROOT.'/vendor/autoload.php';
  62. #require_once 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->prepared_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->prepared_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->prepared_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. // Update LastUpdate every 10 minutes
  269. if (strtotime($UserSessions[$SessionID]['LastUpdate']) + 600 < time()) {
  270. $DB->prepared_query("
  271. UPDATE users_main
  272. SET LastAccess = NOW()
  273. WHERE ID = '$LoggedUser[ID]'
  274. ");
  275. $SessionQuery =
  276. "UPDATE users_sessions
  277. SET ";
  278. // Only update IP if we have an encryption key in memory
  279. if (apcu_exists('DBKEY')) {
  280. $SessionQuery .= "IP = '".Crypto::encrypt($_SERVER['REMOTE_ADDR'])."', ";
  281. }
  282. $SessionQuery .=
  283. "Browser = '$Browser',
  284. OperatingSystem = '$OperatingSystem',
  285. LastUpdate = NOW()
  286. WHERE UserID = '$LoggedUser[ID]'
  287. AND SessionID = '".db_string($SessionID)."'";
  288. $DB->prepared_query($SessionQuery);
  289. $Cache->begin_transaction("users_sessions_$UserID");
  290. $Cache->delete_row($SessionID);
  291. $UsersSessionCache = array(
  292. 'SessionID' => $SessionID,
  293. 'Browser' => $Browser,
  294. 'OperatingSystem' => $OperatingSystem,
  295. 'IP' => (apcu_exists('DBKEY') ? Crypto::encrypt($_SERVER['REMOTE_ADDR']) : $UserSessions[$SessionID]['IP']),
  296. 'LastUpdate' => sqltime() );
  297. $Cache->insert_front($SessionID, $UsersSessionCache);
  298. $Cache->commit_transaction(0);
  299. }
  300. // Notifications
  301. if (isset($LoggedUser['Permissions']['site_torrents_notify'])) {
  302. $LoggedUser['Notify'] = $Cache->get_value('notify_filters_'.$LoggedUser['ID']);
  303. if (!is_array($LoggedUser['Notify'])) {
  304. $DB->prepared_query("
  305. SELECT ID, Label
  306. FROM users_notify_filters
  307. WHERE UserID = '$LoggedUser[ID]'");
  308. $LoggedUser['Notify'] = $DB->to_array('ID');
  309. $Cache->cache_value('notify_filters_'.$LoggedUser['ID'], $LoggedUser['Notify'], 2592000);
  310. }
  311. }
  312. // We've never had to disable the wiki privs of anyone.
  313. if ($LoggedUser['DisableWiki']) {
  314. unset($LoggedUser['Permissions']['site_edit_wiki']);
  315. }
  316. // IP changed
  317. if (apcu_exists('DBKEY') && Crypto::decrypt($LoggedUser['IP']) != $_SERVER['REMOTE_ADDR']) {
  318. if (Tools::site_ban_ip($_SERVER['REMOTE_ADDR'])) {
  319. error('Your IP address has been banned.');
  320. }
  321. $CurIP = db_string($LoggedUser['IP']);
  322. $NewIP = db_string($_SERVER['REMOTE_ADDR']);
  323. $Cache->begin_transaction('user_info_heavy_'.$LoggedUser['ID']);
  324. $Cache->update_row(false, array('IP' => Crypto::encrypt($_SERVER['REMOTE_ADDR'])));
  325. $Cache->commit_transaction(0);
  326. }
  327. // Get stylesheets
  328. $Stylesheets = $Cache->get_value('stylesheets');
  329. if (!is_array($Stylesheets)) {
  330. $DB->prepared_query('
  331. SELECT
  332. ID,
  333. LOWER(REPLACE(Name, " ", "_")) AS Name,
  334. Name AS ProperName,
  335. LOWER(REPLACE(Additions, " ", "_")) AS Additions,
  336. Additions AS ProperAdditions
  337. FROM stylesheets');
  338. $Stylesheets = $DB->to_array('ID', MYSQLI_BOTH);
  339. $Cache->cache_value('stylesheets', $Stylesheets, 0);
  340. }
  341. // todo: Clean up this messy solution
  342. $LoggedUser['StyleName'] = $Stylesheets[$LoggedUser['StyleID']]['Name'];
  343. if (empty($LoggedUser['Username'])) {
  344. logout(); // Ghost
  345. }
  346. }
  347. G::initialize();
  348. $Debug->set_flag('end user handling');
  349. $Debug->set_flag('start function definitions');
  350. /**
  351. * Log out the current session
  352. */
  353. function logout()
  354. {
  355. global $SessionID;
  356. setcookie('session', '', time() - 60 * 60 * 24 * 365, '/', '', false);
  357. setcookie('userid', '', time() - 60 * 60 * 24 * 365, '/', '', false);
  358. setcookie('keeplogged', '', time() - 60 * 60 * 24 * 365, '/', '', false);
  359. if ($SessionID) {
  360. G::$DB->prepared_query("
  361. DELETE FROM users_sessions
  362. WHERE UserID = '" . G::$LoggedUser['ID'] . "'
  363. AND SessionID = '".db_string($SessionID)."'");
  364. G::$Cache->begin_transaction('users_sessions_' . G::$LoggedUser['ID']);
  365. G::$Cache->delete_row($SessionID);
  366. G::$Cache->commit_transaction(0);
  367. }
  368. G::$Cache->delete_value('user_info_' . G::$LoggedUser['ID']);
  369. G::$Cache->delete_value('user_stats_' . G::$LoggedUser['ID']);
  370. G::$Cache->delete_value('user_info_heavy_' . G::$LoggedUser['ID']);
  371. header('Location: login.php');
  372. error();
  373. }
  374. function logout_all_sessions()
  375. {
  376. $UserID = G::$LoggedUser['ID'];
  377. G::$DB->prepared_query("
  378. DELETE FROM users_sessions
  379. WHERE UserID = '$UserID'");
  380. G::$Cache->delete_value('users_sessions_' . $UserID);
  381. logout();
  382. }
  383. function enforce_login()
  384. {
  385. global $SessionID;
  386. if (!$SessionID || !G::$LoggedUser) {
  387. setcookie('redirect', $_SERVER['REQUEST_URI'], time() + 60 * 30, '/', '', false);
  388. logout();
  389. }
  390. }
  391. /**
  392. * Make sure $_GET['auth'] is the same as the user's authorization key
  393. * Should be used for any user action that relies solely on GET.
  394. *
  395. * @param Are we using ajax?
  396. * @return authorisation status. Prints an error message to DEBUG_CHAN on IRC on failure.
  397. */
  398. function authorize($Ajax = false)
  399. {
  400. # Ugly workaround for API tokens
  401. if (!empty($_SERVER['HTTP_AUTHORIZATION']) && $Document === 'ajax') {
  402. return true;
  403. } else {
  404. if (empty($_REQUEST['auth']) || $_REQUEST['auth'] !== G::$LoggedUser['AuthKey']) {
  405. send_irc(DEBUG_CHAN, G::$LoggedUser['Username']." just failed authorize on ".$_SERVER['REQUEST_URI'].(!empty($_SERVER['HTTP_REFERER']) ? " coming from ".$_SERVER['HTTP_REFERER'] : ""));
  406. error('Invalid authorization key. Go back, refresh, and try again.', $NoHTML = true);
  407. return false;
  408. }
  409. }
  410. }
  411. $Debug->set_flag('ending function definitions');
  412. $Document = basename(parse_url($_SERVER['SCRIPT_FILENAME'], PHP_URL_PATH), '.php');
  413. if (!preg_match('/^[a-z0-9]+$/i', $Document)) {
  414. error(404);
  415. }
  416. $StripPostKeys = array_fill_keys(array('password', 'cur_pass', 'new_pass_1', 'new_pass_2', 'verifypassword', 'confirm_password', 'ChangePassword', 'Password'), true);
  417. $Cache->cache_value('php_' . getmypid(), array(
  418. 'start' => sqltime(),
  419. 'document' => $Document,
  420. 'query' => $_SERVER['QUERY_STRING'],
  421. 'get' => $_GET,
  422. 'post' => array_diff_key($_POST, $StripPostKeys)), 600);
  423. // Locked account constant
  424. define('STAFF_LOCKED', 1);
  425. $AllowedPages = ['staffpm', 'ajax', 'locked', 'logout', 'login'];
  426. if (isset(G::$LoggedUser['LockedAccount']) && !in_array($Document, $AllowedPages)) {
  427. require(SERVER_ROOT . '/sections/locked/index.php');
  428. } else {
  429. require(SERVER_ROOT . '/sections/' . $Document . '/index.php');
  430. }
  431. $Debug->set_flag('completed module execution');
  432. // Flush to user
  433. ob_end_flush();
  434. $Debug->set_flag('set headers and send to user');
  435. // Attribute profiling
  436. $Debug->profile();