/** * Internal function to mask portions of the visitor IP address * * @param string $ip IP address in network address format * @param int $maskLength Number of octets to reset */ public static function applyIPMask($ip, $maskLength) { $i = Piwik_Common::strlen($ip); if ($maskLength > $i) { $maskLength = $i; } while ($maskLength-- > 0) { $ip[--$i] = chr(0); } return $ip; }
/** * Sends http request using the specified transport method * * @param string $method * @param string $aUrl * @param int $timeout * @param string $userAgent * @param string $destinationPath * @param resource $file * @param int $followDepth * @param bool|string $acceptLanguage Accept-language header * @param bool $acceptInvalidSslCertificate Only used with $method == 'curl'. If set to true (NOT recommended!) the SSL certificate will not be checked * @throws Exception * @return bool true (or string) on success; false on HTTP response error code (1xx or 4xx) */ public static function sendHttpRequestBy($method = 'socket', $aUrl, $timeout, $userAgent = null, $destinationPath = null, $file = null, $followDepth = 0, $acceptLanguage = false, $acceptInvalidSslCertificate = false) { if ($followDepth > 5) { throw new Exception('Too many redirects (' . $followDepth . ')'); } $contentLength = 0; $fileLength = 0; // Piwik services behave like a proxy, so we should act like one. $xff = 'X-Forwarded-For: ' . (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && !empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] . ',' : '') . Piwik_IP::getIpFromHeader(); if (empty($userAgent)) { $userAgent = self::getUserAgent(); } $via = 'Via: ' . (isset($_SERVER['HTTP_VIA']) && !empty($_SERVER['HTTP_VIA']) ? $_SERVER['HTTP_VIA'] . ', ' : '') . Piwik_Version::VERSION . ' ' . ($userAgent ? " ({$userAgent})" : ''); // proxy configuration $proxyHost = Piwik_Config::getInstance()->proxy['host']; $proxyPort = Piwik_Config::getInstance()->proxy['port']; $proxyUser = Piwik_Config::getInstance()->proxy['username']; $proxyPassword = Piwik_Config::getInstance()->proxy['password']; if ($method == 'socket') { // initialization $url = @parse_url($aUrl); if ($url === false || !isset($url['scheme'])) { throw new Exception('Malformed URL: ' . $aUrl); } if ($url['scheme'] != 'http') { throw new Exception('Invalid protocol/scheme: ' . $url['scheme']); } $host = $url['host']; $port = isset($url['port)']) ? $url['port'] : 80; $path = isset($url['path']) ? $url['path'] : '/'; if (isset($url['query'])) { $path .= '?' . $url['query']; } $errno = null; $errstr = null; $proxyAuth = null; if (!empty($proxyHost) && !empty($proxyPort)) { $connectHost = $proxyHost; $connectPort = $proxyPort; if (!empty($proxyUser) && !empty($proxyPassword)) { $proxyAuth = 'Proxy-Authorization: Basic ' . base64_encode("{$proxyUser}:{$proxyPassword}") . "\r\n"; } $requestHeader = "GET {$aUrl} HTTP/1.1\r\n"; } else { $connectHost = $host; $connectPort = $port; $requestHeader = "GET {$path} HTTP/1.0\r\n"; } // connection attempt if (($fsock = @fsockopen($connectHost, $connectPort, $errno, $errstr, $timeout)) === false || !is_resource($fsock)) { if (is_resource($file)) { @fclose($file); } throw new Exception("Error while connecting to: {$host}. Please try again later. {$errstr}"); } // send HTTP request header $requestHeader .= "Host: {$host}" . ($port != 80 ? ':' . $port : '') . "\r\n" . ($proxyAuth ? $proxyAuth : '') . 'User-Agent: ' . $userAgent . "\r\n" . ($acceptLanguage ? $acceptLanguage . "\r\n" : '') . $xff . "\r\n" . $via . "\r\n" . "Connection: close\r\n" . "\r\n"; fwrite($fsock, $requestHeader); $streamMetaData = array('timed_out' => false); @stream_set_blocking($fsock, true); if (function_exists('stream_set_timeout')) { @stream_set_timeout($fsock, $timeout); } elseif (function_exists('socket_set_timeout')) { @socket_set_timeout($fsock, $timeout); } // process header $status = null; $expectRedirect = false; while (!feof($fsock)) { $line = fgets($fsock, 4096); $streamMetaData = @stream_get_meta_data($fsock); if ($streamMetaData['timed_out']) { if (is_resource($file)) { @fclose($file); } @fclose($fsock); throw new Exception('Timed out waiting for server response'); } // a blank line marks the end of the server response header if (rtrim($line, "\r\n") == '') { break; } // parse first line of server response header if (!$status) { // expect first line to be HTTP response status line, e.g., HTTP/1.1 200 OK if (!preg_match('~^HTTP/(\\d\\.\\d)\\s+(\\d+)(\\s*.*)?~', $line, $m)) { if (is_resource($file)) { @fclose($file); } @fclose($fsock); throw new Exception('Expected server response code. Got ' . rtrim($line, "\r\n")); } $status = (int) $m[2]; // Informational 1xx or Client Error 4xx if ($status < 200 || $status >= 400) { if (is_resource($file)) { @fclose($file); } @fclose($fsock); return false; } continue; } // handle redirect if (preg_match('/^Location:\\s*(.+)/', rtrim($line, "\r\n"), $m)) { if (is_resource($file)) { @fclose($file); } @fclose($fsock); // Successful 2xx vs Redirect 3xx if ($status < 300) { throw new Exception('Unexpected redirect to Location: ' . rtrim($line) . ' for status code ' . $status); } return self::sendHttpRequestBy($method, trim($m[1]), $timeout, $userAgent, $destinationPath, $file, $followDepth + 1, $acceptLanguage); } // save expected content length for later verification if (preg_match('/^Content-Length:\\s*(\\d+)/', $line, $m)) { $contentLength = (int) $m[1]; } } if (feof($fsock)) { throw new Exception('Unexpected end of transmission'); } // process content/body $response = ''; while (!feof($fsock)) { $line = fread($fsock, 8192); $streamMetaData = @stream_get_meta_data($fsock); if ($streamMetaData['timed_out']) { if (is_resource($file)) { @fclose($file); } @fclose($fsock); throw new Exception('Timed out waiting for server response'); } $fileLength += Piwik_Common::strlen($line); if (is_resource($file)) { // save to file fwrite($file, $line); } else { // concatenate to response string $response .= $line; } } // determine success or failure @fclose(@$fsock); } else { if ($method == 'fopen') { $response = false; // we make sure the request takes less than a few seconds to fail // we create a stream_context (works in php >= 5.2.1) // we also set the socket_timeout (for php < 5.2.1) $default_socket_timeout = @ini_get('default_socket_timeout'); @ini_set('default_socket_timeout', $timeout); $ctx = null; if (function_exists('stream_context_create')) { $stream_options = array('http' => array('header' => 'User-Agent: ' . $userAgent . "\r\n" . ($acceptLanguage ? $acceptLanguage . "\r\n" : '') . $xff . "\r\n" . $via . "\r\n", 'max_redirects' => 5, 'timeout' => $timeout)); if (!empty($proxyHost) && !empty($proxyPort)) { $stream_options['http']['proxy'] = 'tcp://' . $proxyHost . ':' . $proxyPort; $stream_options['http']['request_fulluri'] = true; // required by squid proxy if (!empty($proxyUser) && !empty($proxyPassword)) { $stream_options['http']['header'] .= 'Proxy-Authorization: Basic ' . base64_encode("{$proxyUser}:{$proxyPassword}") . "\r\n"; } } $ctx = stream_context_create($stream_options); } // save to file if (is_resource($file)) { $handle = fopen($aUrl, 'rb', false, $ctx); while (!feof($handle)) { $response = fread($handle, 8192); $fileLength += Piwik_Common::strlen($response); fwrite($file, $response); } fclose($handle); } else { $response = @file_get_contents($aUrl, 0, $ctx); $fileLength = Piwik_Common::strlen($response); } // restore the socket_timeout value if (!empty($default_socket_timeout)) { @ini_set('default_socket_timeout', $default_socket_timeout); } } else { if ($method == 'curl') { $ch = @curl_init(); if (!empty($proxyHost) && !empty($proxyPort)) { @curl_setopt($ch, CURLOPT_PROXY, $proxyHost . ':' . $proxyPort); if (!empty($proxyUser) && !empty($proxyPassword)) { // PROXYAUTH defaults to BASIC @curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyUser . ':' . $proxyPassword); } } $curl_options = array(CURLOPT_BINARYTRANSFER => is_resource($file), CURLOPT_URL => $aUrl, CURLOPT_USERAGENT => $userAgent, CURLOPT_HTTPHEADER => array($xff, $via, $acceptLanguage), CURLOPT_HEADER => false, CURLOPT_CONNECTTIMEOUT => $timeout); // Case archive.php is triggering archiving on https:// and the certificate is not valid if ($acceptInvalidSslCertificate) { $curl_options += array(CURLOPT_SSL_VERIFYHOST => false, CURLOPT_SSL_VERIFYPEER => false); } @curl_setopt_array($ch, $curl_options); self::configCurlCertificate($ch); /* * as of php 5.2.0, CURLOPT_FOLLOWLOCATION can't be set if * in safe_mode or open_basedir is set */ if ((string) ini_get('safe_mode') == '' && ini_get('open_basedir') == '') { $curl_options = array(CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5); @curl_setopt_array($ch, $curl_options); } if (is_resource($file)) { // write output directly to file @curl_setopt($ch, CURLOPT_FILE, $file); } else { // internal to ext/curl @curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); } ob_start(); $response = @curl_exec($ch); ob_end_clean(); if ($response === true) { $response = ''; } else { if ($response === false) { $errstr = curl_error($ch); if ($errstr != '') { throw new Exception('curl_exec: ' . $errstr); } $response = ''; } } $contentLength = @curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD); $fileLength = is_resource($file) ? @curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD) : Piwik_Common::strlen($response); @curl_close($ch); unset($ch); } else { throw new Exception('Invalid request method: ' . $method); } } } if (is_resource($file)) { fflush($file); @fclose($file); $fileSize = filesize($destinationPath); if ($contentLength > 0 && $fileLength != $contentLength || $fileSize != $fileLength) { throw new Exception('File size error: ' . $destinationPath . '; expected ' . $contentLength . ' bytes; received ' . $fileLength . ' bytes; saved ' . $fileSize . ' bytes to file'); } return true; } if ($contentLength > 0 && $fileLength != $contentLength) { throw new Exception('Content length error: expected ' . $contentLength . ' bytes; received ' . $fileLength . ' bytes'); } return trim($response); }
/** * Validate "Host" (untrusted user input) * * @param string $host Contents of Host: header from Request * @param array $trustedHosts An array of trusted hosts * * @return boolean True if valid; false otherwise */ public static function isValidHost($host, $trustedHosts) { // Only punctuation we allow is '[', ']', ':', '.' and '-' $hostLength = Piwik_Common::strlen($host); if ($hostLength !== strcspn($host, '`~!@#$%^&*()_+={}\\|;"\'<>,?/ ')) { return false; } $untrustedHost = Piwik_Common::mb_strtolower($host); $hostRegex = Piwik_Common::mb_strtolower(str_replace('.', '\\.', '/(^|.)' . implode('|', $trustedHosts) . '(:[0-9]+)?$/')); return 0 !== preg_match($hostRegex, rtrim($untrustedHost, '.')); }
/** * Determines if an IP address is in a specified IP address range. * * An IPv4-mapped address should be range checked with an IPv4-mapped address range. * * @param string $ip IP address in network address format * @param array $ipRanges List of IP address ranges * @return bool True if in any of the specified IP address ranges; else false. */ public static function isIpInRange($ip, $ipRanges) { $ipLen = Piwik_Common::strlen($ip); if (empty($ip) || empty($ipRanges) || $ipLen != 4 && $ipLen != 16) { return false; } foreach ($ipRanges as $range) { if (is_array($range)) { // already split into low/high IP addresses $range[0] = self::P2N($range[0]); $range[1] = self::P2N($range[1]); } else { // expect CIDR format but handle some variations $range = self::getIpsForRange($range); } if ($range === false) { continue; } $low = $range[0]; $high = $range[1]; if (Piwik_Common::strlen($low) != $ipLen) { continue; } // binary-safe string comparison if ($ip >= $low && $ip <= $high) { return true; } } return false; }
/** * Validate "Host" (untrusted user input) * * @param string|false $host Contents of Host: header from Request. If false, gets the * value from the request. * * @return boolean True if valid; false otherwise */ public static function isValidHost($host = false) { // only do trusted host check if it's enabled if (isset(Piwik_Config::getInstance()->General['enable_trusted_host_check']) && Piwik_Config::getInstance()->General['enable_trusted_host_check'] == 0) { return true; } if ($host === false) { $host = $_SERVER['HTTP_HOST']; if (empty($host)) { return true; } } // if host is in hardcoded whitelist, assume it's valid if (in_array($host, self::$alwaysTrustedHosts)) { return true; } $trustedHosts = @Piwik_Config::getInstance()->General['trusted_hosts']; // if no trusted hosts, just assume it's valid if (empty($trustedHosts)) { self::saveTrustedHostnameInConfig($host); return true; } // Only punctuation we allow is '[', ']', ':', '.' and '-' $hostLength = Piwik_Common::strlen($host); if ($hostLength !== strcspn($host, '`~!@#$%^&*()_+={}\\|;"\'<>,?/ ')) { return false; } foreach ($trustedHosts as &$trustedHost) { $trustedHost = preg_quote($trustedHost); } $untrustedHost = Piwik_Common::mb_strtolower($host); $untrustedHost = rtrim($untrustedHost, '.'); $hostRegex = Piwik_Common::mb_strtolower('/(^|.)' . implode('|', $trustedHosts) . '$/'); $result = preg_match($hostRegex, $untrustedHost); // var_dump($hostRegex);var_dump($untrustedHost);var_dump($result); return 0 !== $result; }