diff --git a/config/cloudflare.ipv4 b/config/cloudflare.ipv4 deleted file mode 100644 index 081a71e..0000000 --- a/config/cloudflare.ipv4 +++ /dev/null @@ -1,14 +0,0 @@ -103.21.244.0/22 -103.22.200.0/22 -103.31.4.0/22 -104.16.0.0/12 -108.162.192.0/18 -141.101.64.0/18 -162.158.0.0/15 -172.64.0.0/13 -173.245.48.0/20 -188.114.96.0/20 -190.93.240.0/20 -197.234.240.0/22 -198.41.128.0/17 -199.27.128.0/21 diff --git a/config/cloudflare.ipv6 b/config/cloudflare.ipv6 deleted file mode 100644 index aa98c77..0000000 --- a/config/cloudflare.ipv6 +++ /dev/null @@ -1,5 +0,0 @@ -2400:cb00::/32 -2405:8100::/32 -2405:b500::/32 -2606:4700::/32 -2803:f800::/32 diff --git a/config/config.example.ini b/config/config.example.ini index e1be930..b73f074 100644 --- a/config/config.example.ini +++ b/config/config.example.ini @@ -33,12 +33,6 @@ prefix = sakura_ ; Data files relative to the root directory [data] -; File containing CloudFlare IPv4 CIDRs -cfipv4 = config/cloudflare.ipv4 - -; File containing CloudFlare IPv6 CIDRs -cfipv6 = config/cloudflare.ipv6 - ; JSON file containing WHOIS servers whoisservers = config/whois.json diff --git a/libraries/Controllers/Meta.php b/libraries/Controllers/Meta.php index b8f5c17..14512ef 100644 --- a/libraries/Controllers/Meta.php +++ b/libraries/Controllers/Meta.php @@ -91,7 +91,7 @@ class Meta Template::vars([ 'page' => [ 'title' => 'Frequently Asked Questions', - 'questions' => Utils::getFaqData(), + 'questions' => Database::fetch('faq', true, null, ['faq_id']), ], ]); @@ -117,7 +117,7 @@ class Meta $id = strtolower($id); // Get info page data from the database - if ($ipData = Utils::loadInfoPage($id)) { + if ($ipData = Database::fetch('infopages', false, ['page_shorthand' => [$id, '=']])) { // Assign new proper variable $renderData['page'] = [ 'id' => $id, diff --git a/libraries/Forum/Post.php b/libraries/Forum/Post.php index 8acdb81..d198512 100644 --- a/libraries/Forum/Post.php +++ b/libraries/Forum/Post.php @@ -12,6 +12,7 @@ use Sakura\Database; use Sakura\User; use Sakura\BBcode; use Sakura\Config; +use Sakura\Net; /** * Used to serve, create and update posts. @@ -212,7 +213,7 @@ class Post 'topic_id' => $thread->id, 'forum_id' => $thread->forum, 'poster_id' => $this->poster->id, - 'poster_ip' => Utils::getRemoteIP(), + 'poster_ip' => Net::pton(Net::IP()), 'post_time' => $this->time, 'post_subject' => $this->subject, 'post_text' => $this->text, diff --git a/libraries/Net.php b/libraries/Net.php new file mode 100644 index 0000000..f8f42d7 --- /dev/null +++ b/libraries/Net.php @@ -0,0 +1,212 @@ + + */ +class Net { + /** + * Returns the connecting IP. + * + * @return string The IP. + */ + public static function IP() + { + return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '::1'; + } + + /** + * Detect the version of an IP. + * + * @param string $ipAddr The IP. + * + * @return int Either 0, 4 or 6. + */ + public static function detectIPVersion($ipAddr) + { + // Check if var is IP + if (filter_var($ipAddr, FILTER_VALIDATE_IP)) { + // v4 + if (filter_var($ipAddr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + return 4; + } + + // v6 + if (filter_var($ipAddr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + return 6; + } + } + + // Not an IP or unknown type + return 0; + } + + /** + * Converts a printable IP address into an unpacked binary string. + * + * @param string $ip Printable IP string. + * + * @throws \Exception Thrown if an invalid IP is supplied. + * + * @return string Unpacked IP address. + */ + public static function pton($ip) + { + // Detect the IP version + $ipv = self::detectIPVersion($ip); + + // Check for IPv4 first since that's most common + if ($ipv === 4) { + return current(unpack("A4", inet_pton($ip))); + } + + // Then attempt IPv6 + if ($ipv === 6) { + return current(unpack("A16", inet_pton($ip))); + } + + // Throw an exception if an invalid IP was supplied + throw new \Exception("Invalid IP address supplied."); + } + + /** + * Converts a binary unpacked IP to a printable packed IP. + * + * @param string $bin The unpacked IP. + * + * @throws \Exception Thrown if the unpacked IP is invalid. + * + * @return string The packed IP. + */ + public static function ntop($bin) + { + // Get the length of the binary string + $len = strlen($bin); + + // Throw an exception if it's not 4 or 16 bytes + if ($len !== 4 && $len !== 16) { + throw new \Exception("Could not handle this IP type."); + } + + // Finally pack the IP + return inet_ntop(pack("A{$len}", $bin)); + } + + public static function matchCIDR($ip, $range) + { + // Break the range up in parts + list($net, $mask) = explode('/', $range); + + // Check IP version + $ipv = self::detectIPVersion($ip); + $rangev = self::detectIPVersion($net); + + // Return false if it's not a valid IP + if ($ipv !== $rangev && !$ipv) { + return false; + } + + // v4 + if ($ipv === 4) { + return self::matchCIDRv4($ip, $net, $mask); + } + + // v6 + if ($ipv === 6) { + return self::matchCIDRv6($ip, $net, $mask); + } + + // Return false by default + return false; + } + + /** + * Match an IPv4 CIDR + * + * @param string $ip The IP address to match. + * @param string $net The Net address to match. + * @param string $mask The Mask to match. + * + * @return bool Returns true if it matches. + */ + private static function matchCIDRv4($ip, $net, $mask) + { + // Convert IP and Net address to long + $ip = ip2long($ip); + $net = ip2long($net); + + // Generate mask + $mask = -1 << (32 - $mask); + + // Do the check + return ($ip & $mask) === $net; + } + + /** + * Converts an IPv6 mask to a byte array + * + * @param int $mask The mask. + * + * @return string The byte array. + */ + private static function maskToByteArray($mask) + { + // Generate an address from the mask + $addr = str_repeat("f", $mask / 4); + + // Append uneven bit + switch ($mask % 4) { + case 1: + $addr .= '8'; + break; + + case 2: + $addr .= 'c'; + break; + + case 3: + $addr .= 'e'; + break; + } + + // Pad the address with zeroes + $addr = str_pad($addr, 32, '0'); + + // Pack the address + $addr = pack('H*', $addr); + + // Return the packed address + return $addr; + } + + /** + * Match an IPv6 CIDR + * + * @param string $ip The IP address to match. + * @param string $net The Net address to match. + * @param int $mask The net mask. + * + * @return bool Returns true if it's a successful match. + */ + private static function matchCIDRv6($ip, $net, $mask) + { + // Pack the IP and Net addresses + $ip = inet_pton($ip); + $net = inet_pton($net); + + // Convert the mask to a byte array + $mask = self::maskToByteArray($mask); + + // Compare them + return ($ip & $mask) === $net; + } +} diff --git a/libraries/Router.php b/libraries/Router.php index 9548244..4bcda5e 100644 --- a/libraries/Router.php +++ b/libraries/Router.php @@ -145,8 +145,10 @@ class Router try { try { return self::$dispatcher->dispatch($method, $url); - } catch (HttpMethodNotAllowedException $e) {} - } catch (HttpRouteNotFoundException $e) {} + } catch (HttpMethodNotAllowedException $e) { + } + } catch (HttpRouteNotFoundException $e) { + } // Default to the not found page return Template::render('global/notfound'); diff --git a/libraries/Session.php b/libraries/Session.php index bbcf324..4633535 100644 --- a/libraries/Session.php +++ b/libraries/Session.php @@ -95,7 +95,7 @@ class Session // Insert the session into the database Database::insert('sessions', [ 'user_id' => $this->userId, - 'user_ip' => Utils::getRemoteIP(), + 'user_ip' => Net::pton(Net::IP()), 'user_agent' => Utils::cleanString(isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'No user agent header.'), 'session_key' => $session, 'session_start' => time(), @@ -132,13 +132,13 @@ class Session } // IP Check - $ipCheck = Config::get('session_check'); + $ipCheck = false;// Config::get('session_check'); // Origin checking if ($ipCheck) { // Split both IPs up $sessionIP = explode('.', $session['user_ip']); - $userIP = explode('.', Utils::getRemoteIP()); + $userIP = explode('.', Net::IP()); // Take 1 off the ipCheck variable so it's equal to the array keys $ipCheck = $ipCheck - 1; diff --git a/libraries/User.php b/libraries/User.php index cbd0af7..faf359b 100644 --- a/libraries/User.php +++ b/libraries/User.php @@ -267,8 +267,8 @@ class User 'password_iter' => $password[1], 'email' => $emailClean, 'rank_main' => 0, - 'register_ip' => Utils::getRemoteIP(), - 'last_ip' => Utils::getRemoteIP(), + 'register_ip' => Net::pton(Net::IP()), + 'last_ip' => Net::pton(Net::IP()), 'user_registered' => time(), 'user_last_online' => 0, 'user_country' => Utils::getCountryCode(), diff --git a/libraries/Users.php b/libraries/Users.php index ebed89f..ea9525d 100644 --- a/libraries/Users.php +++ b/libraries/Users.php @@ -120,7 +120,7 @@ class Users // Check if we haven't hit the rate limit $rates = Database::fetch('login_attempts', true, [ - 'attempt_ip' => [Utils::getRemoteIP(), '='], + 'attempt_ip' => [Net::pton(Net::IP()), '='], 'attempt_timestamp' => [time() - 1800, '>'], 'attempt_success' => [0, '='], ]); diff --git a/libraries/Utils.php b/libraries/Utils.php index 1c26543..7324c3e 100644 --- a/libraries/Utils.php +++ b/libraries/Utils.php @@ -17,16 +17,6 @@ use PHPMailer; */ class Utils { - /** - * Get the emoticons. - * - * @return array Array containing emoticons. - */ - public static function getEmotes() - { - return Database::fetch('emoticons'); - } - /** * Parse the emoticons. * @@ -36,9 +26,8 @@ class Utils */ public static function parseEmotes($text) { - // Get emoticons from the database - $emotes = self::getEmotes(); + $emotes = Database::fetch('emoticons'); // Do the replacements foreach ($emotes as $emote) { @@ -62,7 +51,6 @@ class Utils */ public static function verifyCaptcha($response) { - // Attempt to get the response $resp = @file_get_contents( 'https://www.google.com/recaptcha/api/siteverify?secret=' @@ -93,7 +81,6 @@ class Utils */ public static function errorHandler($errno, $errstr, $errfile, $errline) { - // Remove ROOT path from the error string and file location $errstr = str_replace(ROOT, '', $errstr); $errfile = str_replace(ROOT, '', $errfile); @@ -254,7 +241,6 @@ class Utils */ public static function sendMail($to, $subject, $body) { - // Initialise PHPMailer $mail = new PHPMailer(); @@ -323,7 +309,6 @@ class Utils */ public static function cleanString($string, $lower = false, $noSpecial = false, $replaceSpecial = '') { - // Run common sanitisation function over string $string = htmlentities($string, ENT_NOQUOTES | ENT_HTML401, Config::get('charset')); $string = stripslashes($string); @@ -343,23 +328,6 @@ class Utils return $string; } - /** - * Load contents of an infopage. - * - * @param string $id ID of the info page. - * - * @return array|bool Contents of the info page. - */ - public static function loadInfoPage($id) - { - - // Get contents from the database - $infopage = Database::fetch('infopages', false, ['page_shorthand' => [$id, '=']]); - - // Return the data if there is any else just return false - return count($infopage) ? $infopage : false; - } - /** * Validate MX records. * @@ -369,7 +337,6 @@ class Utils */ public static function checkMXRecord($email) { - // Get the domain from the e-mail address $domain = substr(strstr($email, '@'), 1); @@ -380,174 +347,6 @@ class Utils return $record; } - /** - * Detect the version of an IP. - * - * @param string $ip The IP. - * - * @return int Either 0, 4 or 6. - */ - public static function ipVersion($ip) - { - - // Check if var is IP - if (filter_var($ip, FILTER_VALIDATE_IP)) { - // IPv4 - if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { - return 4; - } - - // IPv6 - if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { - return 6; - } - } - - // Not an IP or unknown type - return 0; - } - - /** - * Unpack an IPv6 - * - * @param mixed $inet IP address. - * - * @return null|string The unpacked IP. - */ - public static function inetToBits($inet) - { - - // Unpack string - $unpacked = unpack('A16', $inet); - - // Split the string - $unpacked = str_split($unpacked[1]); - - // Define variable - $binaryIP = null; - - // "Build" binary IP - foreach ($unpacked as $char) { - $binaryIP .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT); - } - - // Return IP - return $binaryIP; - } - - /** - * Match a subnet. - * - * @param string $ip the IP. - * @param string $range The range. - * - * @return bool|int Success. - */ - public static function matchSubnet($ip, $range) - { - - // Use the proper IP type - switch (self::ipVersion($ip)) { - case 4: - // Break the range up in parts - list($subnet, $bits) = explode('/', $range); - - // Convert IP and Subnet to long - $ip = ip2long($ip); - $subnet = ip2long($subnet); - $mask = -1 << (32 - $bits); - - // In case the supplied subnet wasn't correctly aligned - $subnet &= $mask; - - // Return true if IP is in subnet - return ($ip & $mask) == $subnet; - - case 6: - // Break the range up in parts - list($subnet, $bits) = explode('/', $range); - - // Convert subnet to packed address and convert it to binary - $subnet = inet_pton($subnet); - $binarySubnet = self::inetToBits($subnet); - - // Convert IPv6 to packed address and convert it to binary as well - $ip = inet_pton($ip); - $binaryIP = self::inetToBits($ip); - - // Return bits of the strings according to the bits - $ipBits = substr($binaryIP, 0, $bits); - $subnetBits = substr($binarySubnet, 0, $bits); - - return ($ipBits === $subnetBits); - - default: - return 0; - - } - } - - /** - * Check if an IP originates from CloudFlare. - * - * @param string $ip The IP. - * - * @return bool Success. - */ - public static function checkCFIP($ip) - { - - // Get CloudFlare Subnet list - $cfhosts = file_get_contents( - ROOT . Config::local('data', 'cfipv' . (self::ipVersion($ip))) - ); - - // Replace \r\n with \n - $cfhosts = str_replace("\r\n", "\n", $cfhosts); - - // Explode the file into an array - $cfhosts = explode("\n", $cfhosts); - - // Check if IP is in a CloudFlare subnet - foreach ($cfhosts as $subnet) { - // Check if the subnet isn't empty (git newline prevention) - if (strlen($subnet) < 1) { - continue; - } - - // Return true if found - if (self::matchSubnet($ip, $subnet)) { - return true; - } - } - - // Return false if fails - return false; - } - - /** - * Get the IP of a visitor. - * - * @return string The IP. - */ - public static function getRemoteIP() - { - - // Assign REMOTE_ADDR to a variables - $ip = $_SERVER['REMOTE_ADDR']; - - // Check if the IP is a CloudFlare IP - if (self::checkCFIP($ip)) { - // If it is check if the CloudFlare IP header is set and if it is assign it to the ip variable - if (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) { - $ip = $_SERVER['HTTP_CF_CONNECTING_IP']; - } - } - - // Return the correct IP - return $ip; - } - /** * Get the country code of a visitor. * @@ -557,7 +356,7 @@ class Utils { // Attempt to get country code using PHP's built in geo thing if (function_exists("geoip_country_code_by_name")) { - $code = geoip_country_code_by_name(self::getRemoteIP()); + $code = geoip_country_code_by_name(Net::IP()); // Check if $code is anything if ($code) { @@ -583,7 +382,6 @@ class Utils */ public static function pwdEntropy($pw) { - // Decode utf-8 chars $pw = utf8_decode($pw); @@ -600,7 +398,6 @@ class Utils */ public static function getCountryName($code) { - // Parse JSON file $iso3166 = json_decode( utf8_encode( @@ -620,72 +417,6 @@ class Utils return 'Unknown'; } - /** - * Get the FAQ table data (why is this a function). - * - * @return array FAQ data. - */ - public static function getFaqData() - { - - // Do database call - $faq = Database::fetch('faq', true, null, ['faq_id']); - - // Return FAQ data - return $faq; - } - - /** - * Get the type of log in text (unused and will probably be removwed). - * @param mixed $type - * @return mixed - */ - public static function getLogStringFromType($type) - { - - // Query the database - $return = Database::fetch('logtypes', false, ['id' => [$type, '=']]); - - // Check if type exists and else return a unformattable string - if (count($return) < 2) { - return 'Unknown action.'; - } - - // Return the string - return $return['string']; - } - - /** - * Get a user's logs (unused, probably removed in future). - * @param mixed $uid - * @return array - */ - public static function getUserLogs($uid = 0) - { - - // Check if a user is specified - $conditions = ($uid ? ['uid' => [$uid, '=']] : null); - - // Get data from database - $logsDB = Database::fetch('logs', true, $conditions, ['id', true]); - - // Storage array - $logs = []; - - // Iterate over entries - foreach ($logsDB as $log) { - // Store usable data - $logs[$log['id']] = [ - 'user' => $_USER = Users::getUser($log['uid']), - 'rank' => Users::getRank($_USER['rank_main']), - 'string' => vsprintf(self::getLogStringFromType($log['action']), json_decode($log['attribs'], true)), - ]; - } - - // Return new logs - return $logs; - } - /** * Get the byte symbol for a unit from bytes. * @@ -695,7 +426,6 @@ class Utils */ public static function getByteSymbol($bytes) { - // Return nothing if the input was 0 if (!$bytes) { return; @@ -721,7 +451,6 @@ class Utils */ public static function getPremiumTrackerData() { - // Create data array $data = []; @@ -762,12 +491,10 @@ class Utils public static function updatePremiumTracker($id, $amount, $comment) { Database::insert('premium_log', [ - 'user_id' => $id, 'transaction_amount' => $amount, 'transaction_date' => time(), 'transaction_comment' => $comment, - ]); } diff --git a/public/authenticate.php b/public/authenticate.php index 5901e0d..4beaed5 100644 --- a/public/authenticate.php +++ b/public/authenticate.php @@ -187,7 +187,7 @@ if (isset($_REQUEST['mode'])) { Database::insert('login_attempts', [ 'attempt_success' => $login[0], 'attempt_timestamp' => time(), - 'attempt_ip' => Utils::getRemoteIP(), + 'attempt_ip' => Net::pton(Net::IP()), 'user_id' => isset($login[2]) ? $login[2] : 0, ]); } @@ -315,7 +315,7 @@ if (Users::checkLogin()) { } // Check if a user has already registered from the current IP address -if (count($regUserIP = Users::getUsersByIP(Utils::getRemoteIP()))) { +if (count($regUserIP = Users::getUsersByIP(Net::pton(Net::IP())))) { $renderData['auth']['blockRegister'] = [ 'do' => true, diff --git a/public/content/scripts/sakura.js b/public/content/scripts/sakura.js index 5d89f40..d842f80 100644 --- a/public/content/scripts/sakura.js +++ b/public/content/scripts/sakura.js @@ -118,12 +118,13 @@ var Sakura = (function () { } // Times array var times = { - 31536000: 'year', - 2592000: 'month', - 86400: 'day', - 3600: 'hour', - 60: 'minute', - 1: 'second' + 31536000: ['year', 'a'], + 2592000: ['month', 'a'], + 604800: ['week', 'a'], + 86400: ['day', 'a'], + 3600: ['hour', 'an'], + 60: ['minute', 'a'], + 1: ['second', 'a'] }; // var timeKeys = Object.keys(times).reverse(); @@ -136,7 +137,7 @@ var Sakura = (function () { // Round the number var display = Math.floor(calc); // Return the formatted string - return display + " " + times[timeKeys[i]] + (display === 1 ? '' : 's') + append; + return (display === 1 ? times[timeKeys[i]][1] : display) + " " + times[timeKeys[i]][0] + (display === 1 ? '' : 's') + append; } } // If everything fails just return none diff --git a/public/content/scripts/sakura.ts b/public/content/scripts/sakura.ts index 055b7d5..55739d2 100644 --- a/public/content/scripts/sakura.ts +++ b/public/content/scripts/sakura.ts @@ -141,12 +141,13 @@ class Sakura { // Times array var times: Object = { - 31536000: 'year', - 2592000: 'month', - 86400: 'day', - 3600: 'hour', - 60: 'minute', - 1: 'second' + 31536000: ['year', 'a'], + 2592000: ['month', 'a'], + 604800: ['week', 'a'], + 86400: ['day', 'a'], + 3600: ['hour', 'an'], + 60: ['minute', 'a'], + 1: ['second', 'a'] }; // @@ -163,7 +164,7 @@ class Sakura { var display: number = Math.floor(calc); // Return the formatted string - return display + " " + times[timeKeys[i]] + (display === 1 ? '' : 's') + append; + return (display === 1 ? times[timeKeys[i]][1] : display) + " " + times[timeKeys[i]][0] + (display === 1 ? '' : 's') + append; } } diff --git a/public/index.php b/public/index.php index 06f02b0..db13af1 100644 --- a/public/index.php +++ b/public/index.php @@ -7,7 +7,7 @@ namespace Sakura; // Include components -require_once str_replace(basename(__DIR__), '', dirname(__FILE__)) . 'sakura.php'; +require_once __DIR__ . '/../sakura.php'; // Handle requests echo Router::handle($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']); diff --git a/public/posting.php b/public/posting.php index 5391b96..9b4ed15 100644 --- a/public/posting.php +++ b/public/posting.php @@ -69,7 +69,7 @@ $mode = isset($_GET['f']) ? 'f' : (isset($_GET['t']) ? 't' : (isset($_GET['p']) // Include emotes and bbcodes $posting = [ - 'emoticons' => Utils::getEmotes(), + 'emoticons' => Database::fetch('emoticons'), ]; // Check if we're in reply mode diff --git a/sakura.php b/sakura.php index 7162a74..f78e1ac 100644 --- a/sakura.php +++ b/sakura.php @@ -8,7 +8,7 @@ namespace Sakura; // Define Sakura version -define('SAKURA_VERSION', '20160205'); +define('SAKURA_VERSION', '20160207'); define('SAKURA_VLABEL', 'Amethyst'); define('SAKURA_COLOUR', '#9966CC'); @@ -45,6 +45,7 @@ require_once ROOT . 'libraries/CSRF.php'; require_once ROOT . 'libraries/Database.php'; require_once ROOT . 'libraries/File.php'; require_once ROOT . 'libraries/Hashing.php'; +require_once ROOT . 'libraries/Net.php'; require_once ROOT . 'libraries/News.php'; require_once ROOT . 'libraries/Payments.php'; require_once ROOT . 'libraries/Perms.php';