View file upload/library/XenForo/Helper/Http.php

File size: 7.8Kb
<?php

class XenForo_Helper_Http
{
	/**
	 * Gets a Zend_Http_Client object, automatically switching to cURL if the
	 * specified URL can't be handled by streams.
	 *
	 * @param string $uri
	 * @param array $options
	 *
	 * @return Zend_Http_Client
	 */
	public static function getClient($uri, array $options = array())
	{
		if (!isset($options['adapter']))
		{
			$options += self::getExtraHttpClientOptions($uri);
		}

		if (empty($options['useragent']))
		{
			if (preg_match('/^\d+\.\d+/', XenForo_Application::$version, $versionMatch))
			{
				$version = $versionMatch[0];
			}
			else
			{
				$version = '1.x';
			}
			$options['useragent'] = "XenForo/$version (" . XenForo_Application::getOptions()->boardUrl . ')';
		}

		return new Zend_Http_Client($uri, $options);
	}

	/**
	 * Gets a Zend_Http_Client object designed for use on untrusted URLs. This allows an admin
	 * to configure use of an HTTP proxy (such as with Zend_Http_Client_Adapter_Proxy)
	 *
	 * @param string $uri
	 * @param array $options
	 *
	 * @return Zend_Http_Client
	 */
	public static function getUntrustedClient($uri, array $options = array())
	{
		$uri = self::filterUntrustedUrl($uri);

		if (!self::isRequestableUntrustedUrl($uri, $requestError))
		{
			// use this exception type as it is caught in some cases
			throw new Zend_Uri_Exception("URI '$uri' is not requestable ($requestError)");
		}

		$options['maxredirects'] = 0;

		if (empty($options['useragent']))
		{
			if (preg_match('/^\d+\.\d+/', XenForo_Application::$version, $versionMatch))
			{
				$version = $versionMatch[0];
			}
			else
			{
				$version = '1.x';
			}
			$options['useragent'] = "XenForo/$version (" . XenForo_Application::getOptions()->boardUrl . ')';
		}

		$untrustedConfig = XenForo_Application::getConfig()->untrustedHttpClient;
		$config = $untrustedConfig ? $untrustedConfig->toArray() : array();
		if (!empty($config['adapter']))
		{
			$config += $options;
			return new Zend_Http_Client($uri, $config);
		}
		else
		{
			return self::getClient($uri, $options);
		}
	}

	public static function filterUntrustedUrl($url)
	{
		$url = preg_replace('/#.*$/', '', $url);

		return $url;
	}

	public static function isRequestableUntrustedUrl($url, &$error = null)
	{
		$parts = @parse_url($url);

		if (!$parts || empty($parts['scheme']) || empty($parts['host']))
		{
			$error = 'invalid';
			return false;
		}

		if (!in_array(strtolower($parts['scheme']), array('http', 'https')))
		{
			$error = 'scheme';
			return false;
		}

		if (!empty($parts['port']) && !in_array($parts['port'], array(80, 443)))
		{
			$error = 'port';
			return false;
		}

		if (!empty($parts['user']) || !empty($parts['pass']))
		{
			$error = 'userpass';
			return false;
		}

		if (strpos($parts['host'], '[') !== false)
		{
			$error = 'ipv6';
			return false;
		}

		$hasValidIp = false;

		$ips = @gethostbynamel($parts['host']);
		if ($ips)
		{
			foreach ($ips AS $ip)
			{
				if (self::isLocalIpv4($ip))
				{
					$error = "local: $ip";
					return false;
				}
				else
				{
					$hasValidIp = true;
				}
			}
		}

		if (function_exists('dns_get_record') && defined('DNS_AAAA'))
		{
			$hasIpv6 = defined('AF_INET6');
			if (!$hasIpv6 && function_exists('curl_version') && defined('CURL_VERSION_IPV6'))
			{
				$version = curl_version();
				if ($version['features'] & CURL_VERSION_IPV6)
				{
					$hasIpv6 = true;
				}
			}

			if ($hasIpv6)
			{
				$ipv6s = @dns_get_record($parts['host'], DNS_AAAA);
				if ($ipv6s)
				{
					foreach ($ipv6s AS $ipv6)
					{
						$ip = $ipv6['ipv6'];
						if (self::isLocalIpv6($ip))
						{
							$error = "local: $ip";
							return false;
						}
						else
						{
							$hasValidIp = true;
						}
					}
				}
			}
		}

		if (!$hasValidIp)
		{
			$error = 'dns';
			return false;
		}

		return true;
	}

	public static function isLocalIpv4($ip)
	{
		return preg_match('#^(
			0\.|
			10\.|
			100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.|
			127\.|
			169\.254\.|
			172\.(1[6-9]|2[0-9]|3[01])\.|
			192\.0\.0\.|
			192\.0\.2\.|
			192\.88\.99\.|
			192\.168\.|
			198\.1[89]\.|
			198\.51\.100\.|
			203\.0\.113\.|
			224\.|
			240\.|
			255\.255\.255\.255
		)#x', $ip);
	}

	public static function isLocalIpv6($ip)
	{
		$ip = XenForo_Helper_Ip::convertIpStringToBinary($ip);

		$ranges = array(
			'::' => 128,
			'::1' => 128,
			'::ffff:0:0' => 96,
			'100::' => 64,
			'64:ff9b::' => 96,
			'2001::' => 32,
			'2001:db8::' => 32,
			'2002::' => 16,
			'fc00::' => 7,
			'fe80::' => 10,
			'ff00::' => 8
		);
		foreach ($ranges AS $rangeIp => $cidr)
		{
			$rangeIp = XenForo_Helper_Ip::convertIpStringToBinary($rangeIp);
			if (XenForo_Helper_Ip::ipMatchesCidrRange($ip, $rangeIp, $cidr))
			{
				return true;
			}
		}

		return false;
	}

	public static function getUntrustedWithRedirects($requestUrl, array $config, array $headers = array(), $streamFile = null)
	{
		$requestsMade = 0;
		$hasRedirect = false;

		$config['maxredirects'] = 0;

		do
		{
			if ($hasRedirect && $streamFile)
			{
				// make sure any previous redirect content is wiped out before trying again
				@unlink($streamFile);
			}

			if (preg_match_all('/[^A-Za-z0-9._~:\/?#\[\]@!$&\'()*+,;=%-]/', $requestUrl, $matches))
			{
				foreach ($matches[0] AS $match)
				{
					$requestUrl = str_replace($match[0], '%' . strtoupper(dechex(ord($match[0]))), $requestUrl);
				}
			}
			$requestUrl = preg_replace('/%(?![a-fA-F0-9]{2})/', '%25', $requestUrl);

			$client = XenForo_Helper_Http::getUntrustedClient($requestUrl, $config)
				->setHeaders($headers);

			$response = $client->request('GET');

			$requestsMade++;
			$hasRedirect = false;

			if ($response->isRedirect() && ($location = $response->getHeader('location')))
			{
				if (is_array($location))
				{
					$location = reset($location);
				}

				$requestUrl = XenForo_Helper_Http::getRedirectedUrl($requestUrl, $location);
				$hasRedirect = true;
			}
		}
		while ($requestsMade < 5 && $hasRedirect);

		return $response;
	}

	public static function getRedirectedUrl($originalUrl, $newUrl)
	{
		if (!strlen($newUrl))
		{
			throw new InvalidArgumentException("Empty new URL provided");
		}

		if (Zend_Uri_Http::check($newUrl))
		{
			return $newUrl;
		}

		/** @var Zend_Uri_Http $uri */
		$uri = Zend_Uri::factory($originalUrl);

		if (strpos($newUrl, '?') !== false)
		{
			list($newUrl, $qs) = explode('?', $newUrl, 2);
			if (!strlen($newUrl))
			{
				$qsUrl = preg_replace('#\?.*$#s', '', $originalUrl) . '?' . $qs;
				if (Zend_Uri_Http::check($qsUrl))
				{
					return $qsUrl;
				}
				else
				{
					$newUrl = '/';
				}
			}
		}
		else
		{
			$qs = '';
		}
		$uri->setQuery($qs);

		if ($newUrl[0] === '/')
		{
			// absolute path
			$uri->setPath($newUrl);
		}
		else
		{
			// relative path
			$path = $uri->getPath();
			$lastSlash = strrpos($path, '/');
			if ($lastSlash)
			{
				$path = substr($path, 0, $lastSlash);
			}
			$uri->setPath($path . '/' . $newUrl);
		}

		return $uri->getUri();
	}

	/**
	 * Gets extra options to pass to an HTTP client to ensure it works in more situations
	 *
	 * @param string $uri
	 *
	 * @return array
	 */
	public static function getExtraHttpClientOptions($uri)
	{
		$parts = parse_url($uri);
		$wrappers = stream_get_wrappers();
		if (!in_array($parts['scheme'], $wrappers))
		{
			// can't be handled by sockets -- fallback to cURL
			if (function_exists('curl_getinfo'))
			{
				return array(
					'adapter' => 'Zend_Http_Client_Adapter_Curl',
					'curloptions' => array(CURLOPT_SSL_VERIFYPEER => false)
				);
				// TODO: consider validating SSL cert
			}
		}

		return array();
	}
}