<?php
	// CubicleSoft PHP HTTP class.
	// (C) 2021 CubicleSoft.  All Rights Reserved.

	class HTTP
	{
		// RFC 3986 delimeter splitting implementation with IDNA/Punycode support.
		public static function ExtractURL($url, $idnahost = true)
		{
			$result = array(
				"scheme" => "",
				"authority" => "",
				"login" => "",
				"loginusername" => "",
				"loginpassword" => "",
				"host" => "",
				"port" => "",
				"path" => "",
				"query" => "",
				"queryvars" => array(),
				"fragment" => ""
			);

			$url = str_replace("&amp;", "&", $url);

			$pos = strpos($url, "#");
			if ($pos !== false)
			{
				$result["fragment"] = substr($url, $pos + 1);
				$url = substr($url, 0, $pos);
			}

			$pos = strpos($url, "?");
			if ($pos !== false)
			{
				$result["query"] = str_replace(" ", "+", substr($url, $pos + 1));
				$url = substr($url, 0, $pos);
				$vars = explode("&", $result["query"]);
				foreach ($vars as $var)
				{
					$pos = strpos($var, "=");
					if ($pos === false)
					{
						$name = $var;
						$value = "";
					}
					else
					{
						$name = substr($var, 0, $pos);
						$value = urldecode(substr($var, $pos + 1));
					}
					$name = urldecode($name);
					if (!isset($result["queryvars"][$name]))  $result["queryvars"][$name] = array();
					$result["queryvars"][$name][] = $value;
				}
			}

			$url = str_replace("\\", "/", $url);

			$pos = strpos($url, ":");
			$pos2 = strpos($url, "/");
			if ($pos !== false && ($pos2 === false || $pos < $pos2))
			{
				$result["scheme"] = strtolower(substr($url, 0, $pos));
				$url = substr($url, $pos + 1);
			}

			if (substr($url, 0, 2) != "//")  $result["path"] = $url;
			else
			{
				$url = substr($url, 2);
				$pos = strpos($url, "/");
				if ($pos !== false)
				{
					$result["path"] = substr($url, $pos);
					$url = substr($url, 0, $pos);
				}
				$result["authority"] = $url;

				$pos = strpos($url, "@");
				if ($pos !== false)
				{
					$result["login"] = substr($url, 0, $pos);
					$url = substr($url, $pos + 1);
					$pos = strpos($result["login"], ":");
					if ($pos === false)  $result["loginusername"] = urldecode($result["login"]);
					else
					{
						$result["loginusername"] = urldecode(substr($result["login"], 0, $pos));
						$result["loginpassword"] = urldecode(substr($result["login"], $pos + 1));
					}
				}

				$pos = strpos($url, "]");
				if (substr($url, 0, 1) == "[" && $pos !== false)
				{
					// IPv6 literal address.
					$result["host"] = substr($url, 0, $pos + 1);
					$url = substr($url, $pos + 1);

					$pos = strpos($url, ":");
					if ($pos !== false)
					{
						$result["port"] = substr($url, $pos + 1);
						$url = substr($url, 0, $pos);
					}
				}
				else
				{
					// Normal host[:port].
					$pos = strpos($url, ":");
					if ($pos !== false)
					{
						$result["port"] = substr($url, $pos + 1);
						$url = substr($url, 0, $pos);
					}

					$result["host"] = $url;

					// Handle conversion to Punycode for IDNs.
					if ($idnahost)
					{
						$y = strlen($result["host"]);
						for ($x = 0; $x < $y && ord($result["host"][$x]) <= 0x7F; $x++);

						if ($x < $y)
						{
							if (!class_exists("UTFUtils", false))  require_once str_replace("\\", "/", dirname(__FILE__)) . "/utf_utils.php";

							$host = UTFUtils::ConvertToPunycode($result["host"]);
							if ($host !== false)
							{
								$result["orighost"] = $result["host"];
								$result["host"] = $host;
							}
						}
					}
				}
			}

			return $result;
		}

		// Takes a ExtractURL() array and condenses it into a string.
		public static function CondenseURL($data)
		{
			$result = "";
			if (isset($data["host"]) && $data["host"] != "")
			{
				if (isset($data["scheme"]) && $data["scheme"] != "")  $result = $data["scheme"] . "://";
				if (isset($data["loginusername"]) && $data["loginusername"] != "" && isset($data["loginpassword"]))  $result .= rawurlencode($data["loginusername"]) . ($data["loginpassword"] != "" ? ":" . rawurlencode($data["loginpassword"]) : "") . "@";
				else if (isset($data["login"]) && $data["login"] != "")  $result .= $data["login"] . "@";

				$result .= $data["host"];
				if (isset($data["port"]) && $data["port"] != "")  $result .= ":" . $data["port"];

				if (isset($data["path"]))
				{
					$data["path"] = str_replace("\\", "/", $data["path"]);
					if (substr($data["path"], 0, 1) != "/")  $data["path"] = "/" . $data["path"];
					$result .= $data["path"];
				}
			}
			else if (isset($data["authority"]) && $data["authority"] != "")
			{
				if (isset($data["scheme"]) && $data["scheme"] != "")  $result = $data["scheme"] . "://";

				$result .= $data["authority"];

				if (isset($data["path"]))
				{
					$data["path"] = str_replace("\\", "/", $data["path"]);
					if (substr($data["path"], 0, 1) != "/")  $data["path"] = "/" . $data["path"];
					$result .= $data["path"];
				}
			}
			else if (isset($data["path"]))
			{
				if (isset($data["scheme"]) && $data["scheme"] != "")  $result = $data["scheme"] . ":";

				$result .= $data["path"];
			}

			if (isset($data["query"]))
			{
				if ($data["query"] != "")  $result .= "?" . $data["query"];
			}
			else if (isset($data["queryvars"]))
			{
				$data["query"] = array();
				foreach ($data["queryvars"] as $key => $vals)
				{
					if (is_string($vals))  $vals = array($vals);
					foreach ($vals as $val)  $data["query"][] = urlencode($key) . "=" . urlencode($val);
				}
				$data["query"] = implode("&", $data["query"]);

				if ($data["query"] != "")  $result .= "?" . $data["query"];
			}

			if (isset($data["fragment"]) && $data["fragment"] != "")  $result .= "#" . $data["fragment"];

			return $result;
		}

		public static function ConvertRelativeToAbsoluteURL($baseurl, $relativeurl)
		{
			$relative = (is_array($relativeurl) ? $relativeurl : self::ExtractURL($relativeurl));
			$base = (is_array($baseurl) ? $baseurl : self::ExtractURL($baseurl));

			if ($relative["host"] != "" || ($relative["scheme"] != "" && $relative["scheme"] != $base["scheme"]))
			{
				if ($relative["scheme"] == "")  $relative["scheme"] = $base["scheme"];

				return self::CondenseURL($relative);
			}

			$result = array(
				"scheme" => $base["scheme"],
				"loginusername" => $base["loginusername"],
				"loginpassword" => $base["loginpassword"],
				"host" => $base["host"],
				"port" => $base["port"],
				"path" => "",
				"query" => $relative["query"],
				"fragment" => $relative["fragment"]
			);

			if ($relative["path"] == "")  $result["path"] = $base["path"];
			else if (substr($relative["path"], 0, 1) == "/")  $result["path"] = $relative["path"];
			else
			{
				$abspath = explode("/", $base["path"]);
				array_pop($abspath);
				$relpath = explode("/", $relative["path"]);
				foreach ($relpath as $piece)
				{
					if ($piece == ".")
					{
					}
					else if ($piece == "..")  array_pop($abspath);
					else  $abspath[] = $piece;
				}

				$abspath = implode("/", $abspath);
				if (substr($abspath, 0, 1) != "/")  $abspath = "/" . $abspath;

				$result["path"] = $abspath;
			}

			return self::CondenseURL($result);
		}

		public static function GetUserAgent($type)
		{
			$type = strtolower($type);

			if ($type == "ie")  $type = "ie11";

			// Last updated March 10, 2022.
			if ($type == "ie6")  return "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)";
			else if ($type == "ie7")  return "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 6.0)";
			else if ($type == "ie8")  return "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; SLCC1)";
			else if ($type == "ie9")  return "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)";
			else if ($type == "ie10")  return "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)";
			else if ($type == "ie11")  return "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko";
			else if ($type == "edge")  return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36 Edg/99.0.1150.36";
			else if ($type == "firefox")  return "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0";
			else if ($type == "opera")  return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36 OPR/84.0.4316.21";
			else if ($type == "safari")  return "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.3 Safari/605.1.15";
			else if ($type == "chrome")  return "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36";

			return "";
		}

		public static function GetSSLCiphers($type = "intermediate")
		{
			$type = strtolower($type);

			// Cipher list last updated March 10, 2022.
			// Source:  https://ssl-config.mozilla.org/
			if ($type == "modern")  return "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
			else if ($type == "old")  return "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:DES-CBC3-SHA:HIGH:SEED:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!RSAPSK:!aDH:!aECDH:!EDH-DSS-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA:!SRP";

			return "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS";
		}

		public static function GetSafeSSLOpts($cafile = true, $cipherstype = "intermediate")
		{
			// Result array last updated Feb 15, 2020.
			$result = array(
				"ciphers" => self::GetSSLCiphers($cipherstype),
				"disable_compression" => true,
				"allow_self_signed" => false,
				"verify_peer" => true,
				"verify_depth" => 5,
				"SNI_enabled" => true
			);

			if ($cafile === true)  $result["auto_cainfo"] = true;
			else if ($cafile !== false)  $result["cafile"] = $cafile;

			return $result;
		}

		// Reasonably parses RFC1123, RFC850, and asctime() dates.
		public static function GetDateTimestamp($httpdate)
		{
			$timestamp_map = array(
				"jan" => 1, "feb" => 2, "mar" => 3, "apr" => 4, "may" => 5, "jun" => 6,
				"jul" => 7, "aug" => 8, "sep" => 9, "oct" => 10, "nov" => 11, "dec" => 12
			);

			$year = false;
			$month = false;
			$day = false;
			$hour = false;
			$min = false;
			$sec = false;

			$items = explode(" ", preg_replace('/\s+/', " ", str_replace("-", " ", strtolower($httpdate))));
			foreach ($items as $item)
			{
				if ($item != "")
				{
					if (strpos($item, ":") !== false)
					{
						$item = explode(":", $item);
						$hour = (int)(count($item) > 0 ? array_shift($item) : 0);
						$min = (int)(count($item) > 0 ? array_shift($item) : 0);
						$sec = (int)(count($item) > 0 ? array_shift($item) : 0);

						if ($hour > 23)  $hour = 23;
						if ($min > 59)  $min = 59;
						if ($sec > 59)  $sec = 59;
					}
					else if (is_numeric($item))
					{
						if (strlen($item) >= 4)  $year = (int)$item;
						else if ($day === false)  $day = (int)$item;
						else  $year = substr(date("Y"), 0, 2) . substr($item, -2);
					}
					else
					{
						$item = substr($item, 0, 3);
						if (isset($timestamp_map[$item]))  $month = $timestamp_map[$item];
					}
				}
			}

			if ($year === false || $month === false || $day === false || $hour === false || $min === false || $sec === false)  return false;

			return gmmktime($hour, $min, $sec, $month, $day, $year);
		}

		public static function HTTPTranslate()
		{
			$args = func_get_args();
			if (!count($args))  return "";

			return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args);
		}

		public static function HeaderNameCleanup($name)
		{
			return preg_replace('/\s+/', "-", ucwords(strtolower(trim(preg_replace('/[^A-Za-z0-9 ]/', " ", $name)))));
		}

		private static function HeaderValueCleanup($value)
		{
			return str_replace(array("\r", "\n"), array("", ""), $value);
		}

		public static function NormalizeHeaders($headers)
		{
			$result = array();
			foreach ($headers as $name => $val)
			{
				$val = self::HeaderValueCleanup($val);
				if ($val != "")  $result[self::HeaderNameCleanup($name)] = $val;
			}

			return $result;
		}

		public static function MergeRawHeaders(&$headers, $rawheaders)
		{
			foreach ($rawheaders as $name => $val)
			{
				$val = self::HeaderValueCleanup($val);
				if ($val != "")
				{
					$name2 = self::HeaderNameCleanup($name);
					if (isset($headers[$name2]))  unset($headers[$name2]);

					$headers[$name] = $val;
				}
			}
		}

		public static function ExtractHeader($data)
		{
			$result = array();
			$data = trim($data);
			while ($data != "")
			{
				// Extract name/value pair.
				$pos = strpos($data, "=");
				$pos2 = strpos($data, ";");
				if (($pos !== false && $pos2 === false) || ($pos !== false && $pos2 !== false && $pos < $pos2))
				{
					$name = trim(substr($data, 0, $pos));
					$data = trim(substr($data, $pos + 1));
					if (ord($data[0]) == ord("\""))
					{
						$pos = strpos($data, "\"", 1);
						if ($pos !== false)
						{
							$value = substr($data, 1, $pos - 1);
							$data = trim(substr($data, $pos + 1));
							$pos = strpos($data, ";");
							if ($pos !== false)  $data = substr($data, $pos + 1);
							else  $data = "";
						}
						else
						{
							$value = $data;
							$data = "";
						}
					}
					else
					{
						$pos = strpos($data, ";");
						if ($pos !== false)
						{
							$value = trim(substr($data, 0, $pos));
							$data = substr($data, $pos + 1);
						}
						else
						{
							$value = $data;
							$data = "";
						}
					}
				}
				else if ($pos2 !== false)
				{
					$name = "";
					$value = trim(substr($data, 0, $pos2));
					$data = substr($data, $pos2 + 1);
				}
				else
				{
					$name = "";
					$value = $data;
					$data = "";
				}

				if ($name != "" || $value != "")  $result[strtolower($name)] = $value;

				$data = trim($data);
			}

			return $result;
		}

		private static function ProcessSSLOptions(&$options, $key, $host)
		{
			if (isset($options[$key]["auto_cainfo"]))
			{
				unset($options[$key]["auto_cainfo"]);

				$cainfo = ini_get("curl.cainfo");
				if ($cainfo !== false && strlen($cainfo) > 0)  $options[$key]["cafile"] = $cainfo;
				else if (file_exists(str_replace("\\", "/", dirname(__FILE__)) . "/cacert.pem"))  $options[$key]["cafile"] = str_replace("\\", "/", dirname(__FILE__)) . "/cacert.pem";
			}

			if (isset($options[$key]["auto_peer_name"]))
			{
				unset($options[$key]["auto_peer_name"]);

				if (!isset($options["headers"]["Host"]))  $options[$key]["peer_name"] = $host;
				else
				{
					$info = self::ExtractURL("https://" . $options["headers"]["Host"]);
					$options[$key]["peer_name"] = $info["host"];
				}
			}

			if (isset($options[$key]["auto_cn_match"]))
			{
				unset($options[$key]["auto_cn_match"]);

				if (!isset($options["headers"]["Host"]))  $options[$key]["CN_match"] = $host;
				else
				{
					$info = self::ExtractURL("https://" . $options["headers"]["Host"]);
					$options[$key]["CN_match"] = $info["host"];
				}
			}

			if (isset($options[$key]["auto_sni"]))
			{
				unset($options[$key]["auto_sni"]);

				$options[$key]["SNI_enabled"] = true;
				if (!isset($options["headers"]["Host"]))  $options[$key]["SNI_server_name"] = $host;
				else
				{
					$info = self::ExtractURL("https://" . $options["headers"]["Host"]);
					$options[$key]["SNI_server_name"] = $info["host"];
				}
			}
		}

		// Swiped from str_basics.php so this file can be standalone.
		public static function ExtractFilename($dirfile)
		{
			$dirfile = str_replace("\\", "/", $dirfile);
			$pos = strrpos($dirfile, "/");
			if ($pos !== false)  $dirfile = substr($dirfile, $pos + 1);

			return $dirfile;
		}

		public static function FilenameSafe($filename)
		{
			return preg_replace('/[_]+/', "_", preg_replace('/[^A-Za-z0-9_.\-]/', "_", $filename));
		}

		public static function GetTimeLeft($start, $limit)
		{
			if ($limit === false)  return false;

			$difftime = microtime(true) - $start;
			if ($difftime >= $limit)  return 0;

			return $limit - $difftime;
		}

		private static function ProcessRateLimit($size, $start, $limit, $async)
		{
			$difftime = microtime(true) - $start;
			if ($difftime > 0.0)
			{
				if ($size / $difftime > $limit)
				{
					// Sleeping for some amount of time will equalize the rate.
					// So, solve this for $x:  $size / ($x + $difftime) = $limit
					$amount = ($size - ($limit * $difftime)) / $limit;
					$amount += 0.001;

					if ($async)  return microtime(true) + $amount;
					else  usleep($amount * 1000000);
				}
			}

			return -1.0;
		}

		private static function GetDecodedBody(&$autodecode_ds, $body)
		{
			if ($autodecode_ds !== false)
			{
				$autodecode_ds->Write($body);
				$body = $autodecode_ds->Read();
			}

			return $body;
		}

		private static function StreamTimedOut($fp)
		{
			if (!function_exists("stream_get_meta_data"))  return false;

			$info = stream_get_meta_data($fp);

			return $info["timed_out"];
		}

		public static function InitResponseState($fp, $debug, $options, $startts, $timeout, $result, $close, $nextread, $client = true)
		{
			$state = array(
				"fp" => $fp,
				"type" => "response",
				"async" => (isset($options["async"]) ? $options["async"] : false),
				"debug" => $debug,
				"startts" => $startts,
				"timeout" => $timeout,
				"waituntil" => -1.0,
				"rawdata" => "",
				"data" => "",
				"rawsize" => 0,
				"rawrecvheadersize" => 0,
				"numheaders" => 0,
				"autodecode" => (!isset($options["auto_decode"]) || $options["auto_decode"]),

				"state" => ($client ? "response_line" : "request_line"),

				"options" => $options,
				"result" => $result,
				"close" => $close,
				"nextread" => $nextread,
				"client" => $client
			);

			$state["result"]["recvstart"] = microtime(true);
			$state["result"]["response"] = false;
			$state["result"]["headers"] = false;
			$state["result"]["body"] = false;

			return $state;
		}

		// Handles partially read input.  Also deals with the hacky workaround to the second bugfix in ProcessState__WriteData().
		private static function ProcessState__InternalRead(&$state, $size, $endchar = false)
		{
			$y = strlen($state["nextread"]);

			do
			{
				if ($size <= $y)
				{
					if ($endchar === false)  $pos = $size;
					else
					{
						$pos = strpos($state["nextread"], $endchar);
						if ($pos === false || $pos > $size)  $pos = $size;
						else  $pos++;
					}

					$data = substr($state["nextread"], 0, $pos);
					$state["nextread"] = (string)substr($state["nextread"], $pos);

					return $data;
				}

				if ($endchar !== false)
				{
					$pos = strpos($state["nextread"], $endchar);
					if ($pos !== false)
					{
						$data = substr($state["nextread"], 0, $pos + 1);
						$state["nextread"] = (string)substr($state["nextread"], $pos + 1);

						return $data;
					}
				}

				if ($state["debug"])  $data2 = fread($state["fp"], $size);
				else  $data2 = @fread($state["fp"], $size);

				if ($data2 === false || $data2 === "")
				{
					if ($state["nextread"] === "")  return $data2;

					if ($state["async"] && $endchar !== false && $data2 === "")  return "";

					$data = $state["nextread"];
					$state["nextread"] = "";

					return $data;
				}

				$state["nextread"] .= $data2;

				$y = strlen($state["nextread"]);
			} while (!$state["async"] || ($size <= $y) || ($endchar !== false && strpos($state["nextread"], $endchar) !== false));

			if ($endchar !== false)  return "";

			$data = $state["nextread"];
			$state["nextread"] = "";

			return $data;
		}

		// Reads one line.
		private static function ProcessState__ReadLine(&$state)
		{
			while (strpos($state["data"], "\n") === false)
			{
				$data2 = self::ProcessState__InternalRead($state, 116000, "\n");

				if ($data2 === false || $data2 === "")
				{
					if (feof($state["fp"]))  return array("success" => false, "error" => self::HTTPTranslate("Remote peer disconnected."), "errorcode" => "peer_disconnected");
					else if ($state["async"])  return array("success" => false, "error" => self::HTTPTranslate("Non-blocking read returned no data."), "errorcode" => "no_data");
					else if ($data2 === false)  return array("success" => false, "error" => self::HTTPTranslate("Underlying stream encountered a read error."), "errorcode" => "stream_read_error");
				}
				$pos = strpos($data2, "\n");
				if ($pos === false)
				{
					if (feof($state["fp"]))  return array("success" => false, "error" => self::HTTPTranslate("Remote peer disconnected."), "errorcode" => "peer_disconnected");
					if (self::StreamTimedOut($state["fp"]))  return array("success" => false, "error" => self::HTTPTranslate("Underlying stream timed out."), "errorcode" => "stream_timeout_exceeded");

					$pos = strlen($data2);
				}
				if ($state["timeout"] !== false && self::GetTimeLeft($state["startts"], $state["timeout"]) == 0)  return array("success" => false, "error" => self::HTTPTranslate("HTTP timeout exceeded."), "errorcode" => "timeout_exceeded");
				if (isset($state["options"]["readlinelimit"]) && strlen($state["data"]) + $pos > $state["options"]["readlinelimit"])  return array("success" => false, "error" => self::HTTPTranslate("Read line exceeded limit."), "errorcode" => "read_line_limit_exceeded");

				$state["rawsize"] += strlen($data2);
				$state["data"] .= $data2;

				if (isset($state["options"]["recvlimit"]) && $state["options"]["recvlimit"] < $state["rawsize"])  return array("success" => false, "error" => self::HTTPTranslate("Received data exceeded limit."), "errorcode" => "receive_limit_exceeded");
				if (isset($state["options"]["recvratelimit"]))  $state["waituntil"] = self::ProcessRateLimit($state["rawsize"], $state["recvstart"], $state["options"]["recvratelimit"], $state["async"]);

				if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("rawrecv", $data2, &$state["options"]["debug_callback_opts"]));
				else if ($state["debug"])  $state["rawdata"] .= $data2;
			}

			return array("success" => true);
		}

		// Reads data in.
		private static function ProcessState__ReadBodyData(&$state)
		{
			while ($state["sizeleft"] === false || $state["sizeleft"] > 0)
			{
				$data2 = self::ProcessState__InternalRead($state, ($state["sizeleft"] === false || $state["sizeleft"] > 65536 ? 65536 : $state["sizeleft"]));

				if ($data2 === false)  return array("success" => false, "error" => self::HTTPTranslate("Underlying stream encountered a read error."), "errorcode" => "stream_read_error");
				if ($data2 === "")
				{
					if (feof($state["fp"]))  return array("success" => false, "error" => self::HTTPTranslate("Remote peer disconnected."), "errorcode" => "peer_disconnected");
					if (self::StreamTimedOut($state["fp"]))  return array("success" => false, "error" => self::HTTPTranslate("Underlying stream timed out."), "errorcode" => "stream_timeout_exceeded");

					if ($state["async"])  return array("success" => false, "error" => self::HTTPTranslate("Non-blocking read returned no data."), "errorcode" => "no_data");
				}
				if ($state["timeout"] !== false && self::GetTimeLeft($state["startts"], $state["timeout"]) == 0)  return array("success" => false, "error" => self::HTTPTranslate("HTTP timeout exceeded."), "errorcode" => "timeout_exceeded");

				$tempsize = strlen($data2);
				$state["rawsize"] += $tempsize;
				if ($state["sizeleft"] !== false)  $state["sizeleft"] -= $tempsize;

				if ($state["result"]["response"]["code"] == 100 || !isset($state["options"]["read_body_callback"]) || !is_callable($state["options"]["read_body_callback"]))  $state["result"]["body"] .= self::GetDecodedBody($state["autodecode_ds"], $data2);
				else if (!call_user_func_array($state["options"]["read_body_callback"], array($state["result"][($state["client"] ? "response" : "request")], self::GetDecodedBody($state["autodecode_ds"], $data2), &$state["options"]["read_body_callback_opts"])))  return array("success" => false, "error" => self::HTTPTranslate("Read body callback returned with a failure condition."), "errorcode" => "read_body_callback");

				if (isset($state["options"]["recvlimit"]) && $state["options"]["recvlimit"] < $state["rawsize"])  return array("success" => false, "error" => self::HTTPTranslate("Received data exceeded limit."), "errorcode" => "receive_limit_exceeded");

				if (isset($state["options"]["recvratelimit"]))
				{
					$state["waituntil"] = self::ProcessRateLimit($state["rawsize"], $state["recvstart"], $state["options"]["recvratelimit"], $state["async"]);
					if (microtime(true) < $state["waituntil"])  return array("success" => false, "error" => self::HTTPTranslate("Rate limit for non-blocking connection has not been reached."), "errorcode" => "no_data");
				}

				if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("rawrecv", $data2, &$state["options"]["debug_callback_opts"]));
				else if ($state["debug"])  $state["rawdata"] .= $data2;
			}

			return array("success" => true);
		}

		// Writes data out.
		private static function ProcessState__WriteData(&$state, $prefix)
		{
			if ($state[$prefix . "data"] !== "")
			{
				// Serious bug in PHP core for non-blocking SSL sockets:  https://bugs.php.net/bug.php?id=72333
				if ($state["secure"] && $state["async"] && version_compare(PHP_VERSION, "7.1.4") <= 0)
				{
					// This is a huge hack that has a pretty good chance of blocking on the socket.
					// Peeling off up to just 4KB at a time helps to minimize that possibility.  It's better than guaranteed failure of the socket though.
					@stream_set_blocking($state["fp"], 1);
					if ($state["debug"])  $result = fwrite($state["fp"], (strlen($state[$prefix . "data"]) > 4096 ? substr($state[$prefix . "data"], 0, 4096) : $state[$prefix . "data"]));
					else  $result = @fwrite($state["fp"], (strlen($state[$prefix . "data"]) > 4096 ? substr($state[$prefix . "data"], 0, 4096) : $state[$prefix . "data"]));
					@stream_set_blocking($state["fp"], 0);
				}
				else
				{
					if ($state["debug"])  $result = fwrite($state["fp"], $state[$prefix . "data"]);
					else  $result = @fwrite($state["fp"], $state[$prefix . "data"]);
				}

				if ($result === false || feof($state["fp"]))  return array("success" => false, "error" => self::HTTPTranslate("A fwrite() failure occurred.  Most likely cause:  Connection failure."), "errorcode" => "fwrite_failed");

				// Serious bug in PHP core for all socket types:  https://bugs.php.net/bug.php?id=73535
				if ($result === 0)
				{
					// Temporarily switch to non-blocking sockets and test a one byte read (doesn't matter if data is available or not).
					// This is a bigger hack than the first hack above.
					if (!$state["async"])  @stream_set_blocking($state["fp"], 0);

					if ($state["debug"])  $data2 = fread($state["fp"], 1);
					else  $data2 = @fread($state["fp"], 1);

					if ($data2 === false)  return array("success" => false, "error" => self::HTTPTranslate("Underlying stream encountered a read error."), "errorcode" => "stream_read_error");
					if ($data2 === "" && feof($state["fp"]))  return array("success" => false, "error" => self::HTTPTranslate("Remote peer disconnected."), "errorcode" => "peer_disconnected");

					if ($data2 !== "")  $state["nextread"] .= $data2;

					if (!$state["async"])  @stream_set_blocking($state["fp"], 1);
				}

				if ($state["timeout"] !== false && self::GetTimeLeft($state["startts"], $state["timeout"]) == 0)  return array("success" => false, "error" => self::HTTPTranslate("HTTP timeout exceeded."), "errorcode" => "timeout_exceeded");

				$data2 = (string)substr($state[$prefix . "data"], 0, $result);
				$state[$prefix . "data"] = (string)substr($state[$prefix . "data"], $result);

				$state["result"]["rawsend" . $prefix . "size"] += $result;

				if (isset($state["options"]["sendratelimit"]))
				{
					$state["waituntil"] = self::ProcessRateLimit($state["result"]["rawsendsize"], $state["result"]["connected"], $state["options"]["sendratelimit"], $state["async"]);
					if (microtime(true) < $state["waituntil"])  return array("success" => false, "error" => self::HTTPTranslate("Rate limit for non-blocking connection has not been reached."), "errorcode" => "no_data");
				}

				if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("rawsend", $data2, &$state["options"]["debug_callback_opts"]));
				else if ($state["debug"])  $state["result"]["rawsend"] .= $data2;

				if ($state["async"] && strlen($state[$prefix . "data"]))  return array("success" => false, "error" => self::HTTPTranslate("Non-blocking write did not send all data."), "errorcode" => "no_data");
			}

			return array("success" => true);
		}

		public static function ForceClose(&$state)
		{
			if ($state["fp"] !== false)
			{
				@fclose($state["fp"]);
				$state["fp"] = false;
			}

			if (isset($state["currentfile"]) && $state["currentfile"] !== false)
			{
				if ($state["currentfile"]["fp"] !== false)  @fclose($state["currentfile"]["fp"]);
				$state["currentfile"] = false;
			}
		}

		private static function CleanupErrorState(&$state, $result)
		{
			if (!$result["success"] && $result["errorcode"] !== "no_data")
			{
				self::ForceClose($state);

				$state["error"] = $result;
			}

			return $result;
		}

		public static function WantRead(&$state)
		{
			return ($state["type"] === "response" || $state["state"] === "proxy_connect_response" || $state["state"] === "receive_switch" || $state["state"] === "connecting_enable_crypto" || $state["state"] === "proxy_connect_enable_crypto");
		}

		public static function WantWrite(&$state)
		{
			return (!self::WantRead($state) || $state["state"] === "connecting_enable_crypto" || $state["state"] === "proxy_connect_enable_crypto");
		}

		public static function ProcessState(&$state)
		{
			if (isset($state["error"]))  return $state["error"];

			if ($state["timeout"] !== false && self::GetTimeLeft($state["startts"], $state["timeout"]) == 0)  return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("HTTP timeout exceeded."), "errorcode" => "timeout_exceeded"));
			if (microtime(true) < $state["waituntil"])  return array("success" => false, "error" => self::HTTPTranslate("Rate limit for non-blocking connection has not been reached."), "errorcode" => "no_data");

			if ($state["type"] === "request")
			{
				while ($state["state"] !== "done")
				{
					switch ($state["state"])
					{
						case "connecting":
						{
							if (function_exists("stream_select") && $state["async"])
							{
								$readfp = NULL;
								$writefp = array($state["fp"]);
								$exceptfp = array($state["fp"]);
								if ($state["debug"])  $result = stream_select($readfp, $writefp, $exceptfp, 0);
								else  $result = @stream_select($readfp, $writefp, $exceptfp, 0);
								if ($result === false || count($exceptfp))  return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("A stream_select() failure occurred.  Most likely cause:  Connection failure."), "errorcode" => "stream_select_failed"));

								if (!count($writefp))  return array("success" => false, "error" => self::HTTPTranslate("Connection not established yet."), "errorcode" => "no_data");
							}

							// Deal with failed connections that hang applications.
							if (isset($state["options"]["streamtimeout"]) && $state["options"]["streamtimeout"] !== false && function_exists("stream_set_timeout"))  @stream_set_timeout($state["fp"], $state["options"]["streamtimeout"]);

							// Switch to the next state.
							if ($state["async"] && function_exists("stream_socket_client") && (($state["useproxy"] && $state["proxysecure"]) || (!$state["useproxy"] && $state["secure"])))  $state["state"] = "connecting_enable_crypto";
							else  $state["state"] = "connection_ready";

							if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("nextstate", $state["state"], &$state["options"]["debug_callback_opts"]));

							break;
						}
						case "connecting_enable_crypto":
						{
							// This is only used by clients that connect asynchronously via SSL.
							if ($state["debug"])  $result = stream_socket_enable_crypto($state["fp"], true, STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT);
							else  $result = @stream_socket_enable_crypto($state["fp"], true, STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT);

							if ($result === false)  return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("A stream_socket_enable_crypto() failure occurred.  Most likely cause:  Connection failure or incompatible crypto setup."), "errorcode" => "stream_socket_enable_crypto_failed"));
							else if ($result === true)  $state["state"] = "connection_ready";
							else  return array("success" => false, "error" => self::HTTPTranslate("Non-blocking enable crypto operation is not complete yet."), "errorcode" => "no_data");

							if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("nextstate", $state["state"], &$state["options"]["debug_callback_opts"]));

							break;
						}
						case "connection_ready":
						{
							// Handle peer certificate retrieval.
							if (function_exists("stream_context_get_options"))
							{
								$contextopts = stream_context_get_options($state["fp"]);
								if ($state["useproxy"])
								{
									if ($state["proxysecure"] && isset($state["options"]["proxysslopts"]) && is_array($state["options"]["proxysslopts"]))
									{
										if (isset($state["options"]["peer_cert_callback"]) && is_callable($state["options"]["peer_cert_callback"]))
										{
											if (isset($contextopts["ssl"]["peer_certificate"]) && !call_user_func_array($state["options"]["peer_cert_callback"], array("proxypeercert", $contextopts["ssl"]["peer_certificate"], &$state["options"]["peer_cert_callback_opts"])))  return array("success" => false, "error" => self::HTTPTranslate("Peer certificate callback returned with a failure condition."), "errorcode" => "peer_cert_callback");
											if (isset($contextopts["ssl"]["peer_certificate_chain"]) && !call_user_func_array($state["options"]["peer_cert_callback"], array("proxypeercertchain", $contextopts["ssl"]["peer_certificate_chain"], &$state["options"]["peer_cert_callback_opts"])))  return array("success" => false, "error" => self::HTTPTranslate("Peer certificate callback returned with a failure condition."), "errorcode" => "peer_cert_callback");
										}
									}
								}
								else
								{
									if ($state["secure"] && isset($state["options"]["sslopts"]) && is_array($state["options"]["sslopts"]))
									{
										if (isset($state["options"]["peer_cert_callback"]) && is_callable($state["options"]["peer_cert_callback"]))
										{
											if (isset($contextopts["ssl"]["peer_certificate"]) && !call_user_func_array($state["options"]["peer_cert_callback"], array("peercert", $contextopts["ssl"]["peer_certificate"], &$state["options"]["peer_cert_callback_opts"])))  return array("success" => false, "error" => self::HTTPTranslate("Peer certificate callback returned with a failure condition."), "errorcode" => "peer_cert_callback");
											if (isset($contextopts["ssl"]["peer_certificate_chain"]) && !call_user_func_array($state["options"]["peer_cert_callback"], array("peercertchain", $contextopts["ssl"]["peer_certificate_chain"], &$state["options"]["peer_cert_callback_opts"])))  return array("success" => false, "error" => self::HTTPTranslate("Peer certificate callback returned with a failure condition."), "errorcode" => "peer_cert_callback");
										}
									}
								}
							}

							$state["result"]["connected"] = microtime(true);

							// Switch to the correct state.
							if ($state["proxyconnect"])
							{
								$state["result"]["rawsendproxysize"] = 0;
								$state["result"]["rawsendproxyheadersize"] = strlen($state["proxydata"]);

								$state["state"] = "proxy_connect_send";
							}
							else
							{
								$state["result"]["sendstart"] = microtime(true);

								$state["state"] = "send_data";
							}

							if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("nextstate", $state["state"], &$state["options"]["debug_callback_opts"]));

							break;
						}
						case "proxy_connect_send":
						{
							// Send the HTTP CONNECT request to the proxy.
							$result = self::ProcessState__WriteData($state, "proxy");
							if (!$result["success"])  return self::CleanupErrorState($state, $result);

							// Prepare the state for handling the response from the proxy server.
							$options2 = array();
							if (isset($state["options"]["async"]))  $options2["async"] = $state["options"]["async"];
							if (isset($state["options"]["recvratelimit"]))  $options2["recvratelimit"] = $state["options"]["recvratelimit"];
							if (isset($state["options"]["debug_callback"]))
							{
								$options2["debug_callback"] = $state["options"]["debug_callback"];
								$options2["debug_callback_opts"] = $state["options"]["debug_callback_opts"];
							}
							$state["proxyresponse"] = self::InitResponseState($state["fp"], $state["debug"], $options2, $state["startts"], $state["timeout"], $state["result"], false, $state["nextread"]);
							$state["proxyresponse"]["proxyconnect"] = true;

							$state["state"] = "proxy_connect_response";

							if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("nextstate", $state["state"], &$state["options"]["debug_callback_opts"]));

							break;
						}
						case "proxy_connect_response":
						{
							// Recursively call this function to handle the proxy response.
							$result = self::ProcessState($state["proxyresponse"]);
							if (!$result["success"])  return self::CleanupErrorState($state, $result);

							$state["result"]["rawrecvsize"] += $result["rawrecvsize"];
							$state["result"]["rawrecvheadersize"] += $result["rawrecvheadersize"];

							if (substr($result["response"]["code"], 0, 1) != "2")  return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("Expected a 200 response from the CONNECT request.  Received:  %s.", $result["response"]["line"]), "info" => $result, "errorcode" => "proxy_connect_tunnel_failed"));

							// Proxy connect tunnel established.  Proceed normally.
							$state["result"]["sendstart"] = microtime(true);

							if ($state["secure"])  $state["state"] = "proxy_connect_enable_crypto";
							else  $state["state"] = "send_data";

							if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("nextstate", $state["state"], &$state["options"]["debug_callback_opts"]));

							break;
						}
						case "proxy_connect_enable_crypto":
						{
							if ($state["debug"])  $result = stream_socket_enable_crypto($state["fp"], true, STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT);
							else  $result = @stream_socket_enable_crypto($state["fp"], true, STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT);

							if ($result === false)  return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("A stream_socket_enable_crypto() failure occurred.  Most likely cause:  Tunnel connection failure or incompatible crypto setup."), "errorcode" => "stream_socket_enable_crypto_failed"));
							else if ($result === true)
							{
								// Handle peer certificate retrieval.
								if (function_exists("stream_context_get_options"))
								{
									$contextopts = stream_context_get_options($state["fp"]);

									if (isset($state["options"]["sslopts"]) && is_array($state["options"]["sslopts"]))
									{
										if (isset($state["options"]["peer_cert_callback"]) && is_callable($state["options"]["peer_cert_callback"]))
										{
											if (isset($contextopts["ssl"]["peer_certificate"]) && !call_user_func_array($state["options"]["peer_cert_callback"], array("peercert", $contextopts["ssl"]["peer_certificate"], &$state["options"]["peer_cert_callback_opts"])))  return array("success" => false, "error" => self::HTTPTranslate("Peer certificate callback returned with a failure condition."), "errorcode" => "peer_cert_callback");
											if (isset($contextopts["ssl"]["peer_certificate_chain"]) && !call_user_func_array($state["options"]["peer_cert_callback"], array("peercertchain", $contextopts["ssl"]["peer_certificate_chain"], &$state["options"]["peer_cert_callback_opts"])))  return array("success" => false, "error" => self::HTTPTranslate("Peer certificate callback returned with a failure condition."), "errorcode" => "peer_cert_callback");
										}
									}
								}

								// Secure connection established.
								$state["state"] = "send_data";

								if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("nextstate", $state["state"], &$state["options"]["debug_callback_opts"]));
							}

							break;
						}
						case "send_data":
						{
							// Send the queued data.
							$result = self::ProcessState__WriteData($state, "");
							if (!$result["success"])  return self::CleanupErrorState($state, $result);

							// Queue up more data.
							if (isset($state["options"]["write_body_callback"]) && is_callable($state["options"]["write_body_callback"]))
							{
								if ($state["bodysize"] === false || $state["bodysize"] > 0)
								{
									$bodysize2 = $state["bodysize"];
									$result = call_user_func_array($state["options"]["write_body_callback"], array(&$state["data"], &$bodysize2, &$state["options"]["write_body_callback_opts"]));
									if (!$result || ($state["bodysize"] !== false && strlen($state["data"]) > $state["bodysize"]))  return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("HTTP write body callback function failed."), "errorcode" => "write_body_callback"));

									if ($state["bodysize"] === false)
									{
										if ($state["data"] !== "" && $state["chunked"])  $state["data"] = dechex(strlen($state["data"])) . "\r\n" . $state["data"] . "\r\n";

										// When $bodysize2 is set to true, it is the last chunk.
										if ($bodysize2 === true)
										{
											if ($state["chunked"])
											{
												$state["data"] .= "0\r\n";

												// Allow the body callback function to append additional headers to the content to send.
												// It is up to the callback function to correctly format the extra headers.
												$result = call_user_func_array($state["options"]["write_body_callback"], array(&$state["data"], &$bodysize2, &$state["options"]["write_body_callback_opts"]));
												if (!$result)  return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("HTTP write body callback function failed."), "errorcode" => "write_body_callback"));

												$state["data"] .= "\r\n";
											}

											$state["bodysize"] = 0;
										}
									}
									else
									{
										$state["bodysize"] -= strlen($state["data"]);
									}
								}
							}
							else if (isset($state["options"]["files"]) && $state["bodysize"] > 0)
							{
								// Select the next file to upload.
								if ($state["currentfile"] === false && count($state["options"]["files"]))
								{
									$state["currentfile"] = array_shift($state["options"]["files"]);

									$name = self::HeaderValueCleanup($state["currentfile"]["name"]);
									$name = str_replace("\"", "", $name);
									$filename = self::FilenameSafe(self::ExtractFilename($state["currentfile"]["filename"]));
									$type = self::HeaderValueCleanup($state["currentfile"]["type"]);

									$state["data"] = "--" . $state["mime"] . "\r\n";
									$state["data"] .= "Content-Disposition: form-data; name=\"" . $name . "\"; filename=\"" . $filename . "\"\r\n";
									$state["data"] .= "Content-Type: " . $type . "\r\n";
									$state["data"] .= "\r\n";

									if (!isset($state["currentfile"]["datafile"]))
									{
										$state["data"] .= $state["currentfile"]["data"];
										$state["data"] .= "\r\n";

										$state["currentfile"] = false;
									}
									else
									{
										$state["currentfile"]["fp"] = @fopen($state["currentfile"]["datafile"], "rb");
										if ($state["currentfile"]["fp"] === false)  return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("The file '%s' does not exist.", $state["currentfile"]["datafile"]), "errorcode" => "file_does_not_exist"));
									}
								}

								// Process the next chunk of file information.
								if ($state["currentfile"] !== false && isset($state["currentfile"]["fp"]))
								{
									// Read/Write up to 65K at a time.
									if ($state["currentfile"]["filesize"] >= 65536)
									{
										$data2 = fread($state["currentfile"]["fp"], 65536);
										if ($data2 === false || strlen($data2) !== 65536)  return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("A read error was encountered with the file '%s'.", $state["currentfile"]["datafile"]), "errorcode" => "file_read"));

										$state["data"] .= $data2;

										$state["currentfile"]["filesize"] -= 65536;
									}
									else
									{
										// Read in the rest.
										if ($state["currentfile"]["filesize"] > 0)
										{
											$data2 = fread($state["currentfile"]["fp"], $state["currentfile"]["filesize"]);
											if ($data2 === false || strlen($data2) != $state["currentfile"]["filesize"])  return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("A read error was encountered with the file '%s'.", $state["currentfile"]["datafile"]), "errorcode" => "file_read"));

											$state["data"] .= $data2;
										}

										$state["data"] .= "\r\n";

										fclose($state["currentfile"]["fp"]);

										$state["currentfile"] = false;
									}
								}

								// If there is no more data, write out the closing MIME line.
								if ($state["data"] === "")  $state["data"] = "--" . $state["mime"] . "--\r\n";

								$state["bodysize"] -= strlen($state["data"]);
							}
							else if ($state["bodysize"] === false || $state["bodysize"] > 0)
							{
								return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("A weird internal HTTP error that should never, ever happen...just happened."), "errorcode" => "impossible"));
							}

							// All done sending data.
							if ($state["data"] === "")
							{
								if ($state["client"])
								{
									$state["state"] = "receive_switch";

									if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("nextstate", $state["state"], &$state["options"]["debug_callback_opts"]));
								}
								else
								{
									$state["result"]["endts"] = microtime(true);

									if ($state["close"])  fclose($state["fp"]);
									else  $state["result"]["fp"] = $state["fp"];

									return $state["result"];
								}
							}

							break;
						}
						case "receive_switch":
						{
							if (function_exists("stream_select") && $state["async"])
							{
								$readfp = array($state["fp"]);
								$writefp = NULL;
								$exceptfp = NULL;
								if ($state["debug"])  $result = stream_select($readfp, $writefp, $exceptfp, 0);
								else  $result = @stream_select($readfp, $writefp, $exceptfp, 0);
								if ($result === false)  return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("A stream_select() failure occurred.  Most likely cause:  Connection failure."), "errorcode" => "stream_select_failed"));

								if (!count($readfp))  return array("success" => false, "error" => self::HTTPTranslate("Connection not fully established yet."), "errorcode" => "no_data");
							}

							$state["state"] = "done";

							if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("nextstate", $state["state"], &$state["options"]["debug_callback_opts"]));

							break;
						}
					}
				}

				// The request has been sent.  Change the state to a response state.
				$state = self::InitResponseState($state["fp"], $state["debug"], $state["options"], $state["startts"], $state["timeout"], $state["result"], $state["close"], $state["nextread"]);

				// Run one cycle.
				return self::ProcessState($state);
			}
			else if ($state["type"] === "response")
			{
				while ($state["state"] !== "done")
				{
					switch ($state["state"])
					{
						case "response_line":
						{
							$result = self::ProcessState__ReadLine($state);
							if (!$result["success"])  return self::CleanupErrorState($state, $result);

							// Parse the response line.
							$pos = strpos($state["data"], "\n");
							if ($pos === false)  return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("Unable to retrieve response line."), "errorcode" => "get_response_line"));
							$line = trim(substr($state["data"], 0, $pos));
							$state["data"] = substr($state["data"], $pos + 1);
							$state["rawrecvheadersize"] += $pos + 1;
							$response = explode(" ", $line, 3);

							$state["result"]["response"] = array(
								"line" => $line,
								"httpver" => strtoupper($response[0]),
								"code" => $response[1],
								"meaning" => (isset($response[2]) ? $response[2] : "")
							);

							$state["state"] = "headers";
							$state["result"]["headers"] = array();
							$state["lastheader"] = "";

							if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("nextstate", $state["state"], &$state["options"]["debug_callback_opts"]));

							break;
						}
						case "request_line":
						{
							// Server mode only.
							$result = self::ProcessState__ReadLine($state);
							if (!$result["success"])  return self::CleanupErrorState($state, $result);

							// Parse the request line.
							$pos = strpos($state["data"], "\n");
							if ($pos === false)  return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("Unable to retrieve request line."), "errorcode" => "get_request_line"));
							$line = trim(substr($state["data"], 0, $pos));
							$state["data"] = substr($state["data"], $pos + 1);
							$state["rawrecvheadersize"] += $pos + 1;

							$request = $line;
							$pos = strpos($request, " ");
							if ($pos === false)  $pos = strlen($request);
							$method = (string)substr($request, 0, $pos);
							$request = trim(substr($request, $pos));

							$pos = strrpos($request, " ");
							if ($pos === false)  $pos = strlen($request);
							$path = trim(substr($request, 0, $pos));
							if ($path === "")  $path = "/";
							$version = (string)substr($request, $pos + 1);

							$state["result"]["request"] = array(
								"line" => $line,
								"method" => strtoupper($method),
								"path" => $path,
								"httpver" => strtoupper($version),
							);

							// Fake the response line to bypass some client-only code.
							$state["result"]["response"] = array(
								"line" => "200",
								"httpver" => "",
								"code" => 200,
								"meaning" => ""
							);

							$state["state"] = "headers";
							$state["result"]["headers"] = array();
							$state["lastheader"] = "";

							if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("nextstate", $state["state"], &$state["options"]["debug_callback_opts"]));

							break;
						}
						case "headers":
						case "body_chunked_headers":
						{
							$result = self::ProcessState__ReadLine($state);
							if (!$result["success"] && ($state["state"] === "headers" || ($result["errorcode"] !== "stream_read_error" && $result["errorcode"] !== "peer_disconnected")))  return self::CleanupErrorState($state, $result);

							$pos = strpos($state["data"], "\n");
							if ($pos === false)  $pos = strlen($state["data"]);
							$header = rtrim(substr($state["data"], 0, $pos));
							$state["data"] = substr($state["data"], $pos + 1);
							$state["rawrecvheadersize"] += $pos + 1;
							if ($header != "")
							{
								if ($state["lastheader"] != "" && (substr($header, 0, 1) == " " || substr($header, 0, 1) == "\t"))  $state["result"]["headers"][$state["lastheader"]][count($state["result"]["headers"][$state["lastheader"]]) - 1] .= $header;
								else
								{
									$pos = strpos($header, ":");
									if ($pos === false)  $pos = strlen($header);
									$state["lastheader"] = self::HeaderNameCleanup(substr($header, 0, $pos));
									if (!isset($state["result"]["headers"][$state["lastheader"]]))  $state["result"]["headers"][$state["lastheader"]] = array();
									$state["result"]["headers"][$state["lastheader"]][] = ltrim(substr($header, $pos + 1));
								}

								$state["numheaders"]++;
								if (isset($state["options"]["maxheaders"]) && $state["numheaders"] > $state["options"]["maxheaders"])  return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("The number of headers exceeded the limit."), "errorcode" => "headers_limit_exceeded"));
							}
							else
							{
								if ($state["result"]["response"]["code"] != 100 && isset($state["options"]["read_headers_callback"]) && is_callable($state["options"]["read_headers_callback"]))
								{
									if (!call_user_func_array($state["options"]["read_headers_callback"], array(&$state["result"][($state["client"] ? "response" : "request")], &$state["result"]["headers"], &$state["options"]["read_headers_callback_opts"])))  return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("Read headers callback returned with a failure condition."), "errorcode" => "read_header_callback"));
								}

								// Additional headers (optional) are the last bit of data in a chunked response.
								if ($state["state"] === "body_chunked_headers")
								{
									$state["state"] = "body_finalize";

									if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("nextstate", $state["state"], &$state["options"]["debug_callback_opts"]));
								}
								else
								{
									$state["result"]["body"] = "";

									// Handle 100 Continue below OR WebSocket among other things by letting the caller handle reading the body.
									if ($state["result"]["response"]["code"] == 100 || $state["result"]["response"]["code"] == 101)
									{
										$state["state"] = "done";

										if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("nextstate", $state["state"], &$state["options"]["debug_callback_opts"]));
									}
									else
									{
										// Determine if decoding the content is possible and necessary.
										if ($state["autodecode"] && !isset($state["result"]["headers"]["Content-Encoding"]) || (strtolower($state["result"]["headers"]["Content-Encoding"][0]) != "gzip" && strtolower($state["result"]["headers"]["Content-Encoding"][0]) != "deflate"))  $state["autodecode"] = false;
										if (!$state["autodecode"])  $state["autodecode_ds"] = false;
										else
										{
											if (!class_exists("DeflateStream", false))  require_once str_replace("\\", "/", dirname(__FILE__)) . "/deflate_stream.php";

											// Since servers and browsers do everything wrong, ignore the encoding claim and attempt to auto-detect the encoding.
											$state["autodecode_ds"] = new DeflateStream();
											$state["autodecode_ds"]->Init("rb", -1, array("type" => "auto"));
										}

										// Use the appropriate state for handling the next bit of input.
										if (isset($state["result"]["headers"]["Transfer-Encoding"]) && strtolower($state["result"]["headers"]["Transfer-Encoding"][0]) == "chunked")
										{
											$state["state"] = "body_chunked_size";
										}
										else
										{
											$state["sizeleft"] = (isset($state["result"]["headers"]["Content-Length"]) ? (double)preg_replace('/[^0-9]/', "", $state["result"]["headers"]["Content-Length"][0]) : false);
											$state["state"] = (!isset($state["proxyconnect"]) && ($state["sizeleft"] !== false || $state["client"]) ? "body_content" : "done");
										}

										if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("nextstate", $state["state"], &$state["options"]["debug_callback_opts"]));

										// Let servers have a chance to alter limits before processing the input body.
										if (!$state["client"] && $state["state"] !== "done")  return array("success" => false, "error" => self::HTTPTranslate("Intermission for adjustments to limits."), "errorcode" => "no_data");
									}
								}
							}

							break;
						}
						case "body_chunked_size":
						{
							$result = self::ProcessState__ReadLine($state);
							if (!$result["success"])  return self::CleanupErrorState($state, $result);

							$pos = strpos($state["data"], "\n");
							if ($pos === false)  $pos = strlen($state["data"]);
							$line = trim(substr($state["data"], 0, $pos));
							$state["data"] = substr($state["data"], $pos + 1);
							$pos = strpos($line, ";");
							if ($pos === false)  $pos = strlen($line);
							$size = hexdec(substr($line, 0, $pos));
							if ($size < 0)  $size = 0;

							// Retrieve content.
							$size2 = $size;
							$size3 = min(strlen($state["data"]), $size);
							if ($size3 > 0)
							{
								$data2 = substr($state["data"], 0, $size3);
								$state["data"] = substr($state["data"], $size3);
								$size2 -= $size3;

								if ($state["result"]["response"]["code"] == 100 || !isset($state["options"]["read_body_callback"]) || !is_callable($state["options"]["read_body_callback"]))  $state["result"]["body"] .= self::GetDecodedBody($state["autodecode_ds"], $data2);
								else if (!call_user_func_array($state["options"]["read_body_callback"], array($state["result"][($state["client"] ? "response" : "request")], self::GetDecodedBody($state["autodecode_ds"], $data2), &$state["options"]["read_body_callback_opts"])))  return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("Read body callback returned with a failure condition."), "errorcode" => "read_body_callback"));
							}

							$state["chunksize"] = $size;
							$state["sizeleft"] = $size2;
							$state["state"] = "body_chunked_data";

							if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("nextstate", $state["state"], &$state["options"]["debug_callback_opts"]));

							break;
						}
						case "body_chunked_data":
						{
							$result = self::ProcessState__ReadBodyData($state);
							if (!$result["success"])  return self::CleanupErrorState($state, $result);

							if ($state["chunksize"] > 0)  $state["state"] = "body_chunked_skipline";
							else
							{
								$state["lastheader"] = "";
								$state["state"] = "body_chunked_headers";
							}

							if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("nextstate", $state["state"], &$state["options"]["debug_callback_opts"]));

							break;
						}
						case "body_chunked_skipline":
						{
							$result = self::ProcessState__ReadLine($state);
							if (!$result["success"])  return self::CleanupErrorState($state, $result);

							// Ignore one newline.
							$pos = strpos($state["data"], "\n");
							if ($pos === false)  $pos = strlen($state["data"]);
							$state["data"] = substr($state["data"], $pos + 1);

							$state["state"] = "body_chunked_size";

							if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("nextstate", $state["state"], &$state["options"]["debug_callback_opts"]));

							break;
						}
						case "body_content":
						{
							$result = self::ProcessState__ReadBodyData($state);
							if (!$result["success"] && (($state["sizeleft"] !== false && $state["sizeleft"] > 0) || ($state["sizeleft"] === false && $result["errorcode"] !== "stream_read_error" && $result["errorcode"] !== "peer_disconnected" && $result["errorcode"] !== "stream_timeout_exceeded")))  return self::CleanupErrorState($state, $result);

							$state["state"] = "body_finalize";

							if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("nextstate", $state["state"], &$state["options"]["debug_callback_opts"]));

							break;
						}
						case "body_finalize":
						{
							if ($state["autodecode_ds"] !== false)
							{
								$state["autodecode_ds"]->Finalize();
								$data2 = $state["autodecode_ds"]->Read();

								if ($state["result"]["response"]["code"] == 100 || !isset($state["options"]["read_body_callback"]) || !is_callable($state["options"]["read_body_callback"]))  $state["result"]["body"] .= $data2;
								else if (!call_user_func_array($state["options"]["read_body_callback"], array($state["result"][($state["client"] ? "response" : "request")], $data2, &$state["options"]["read_body_callback_opts"])))  return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("Read body callback returned with a failure condition."), "errorcode" => "read_body_callback"));
							}

							$state["state"] = "done";

							if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("nextstate", $state["state"], &$state["options"]["debug_callback_opts"]));

							break;
						}
					}

					// Handle HTTP 100 Continue status codes.
					if ($state["state"] === "done" && $state["result"]["response"]["code"] == 100)
					{
						$state["autodecode"] = (!isset($state["options"]["auto_decode"]) || $state["options"]["auto_decode"]);
						$state["state"] = "response";
						$state["result"]["response"] = false;
						$state["result"]["headers"] = false;
						$state["result"]["body"] = false;

						if (isset($state["options"]["debug_callback"]) && is_callable($state["options"]["debug_callback"]))  call_user_func_array($state["options"]["debug_callback"], array("nextstate", $state["state"], &$state["options"]["debug_callback_opts"]));
					}
				}

				if ($state["debug"])  $state["result"]["rawrecv"] .= $state["rawdata"];
				$state["result"]["rawrecvsize"] += $state["rawsize"];
				$state["result"]["rawrecvheadersize"] += $state["rawrecvheadersize"];
				$state["result"]["endts"] = microtime(true);

				if ($state["close"] || ($state["client"] && isset($state["result"]["headers"]["Connection"]) && strtolower($state["result"]["headers"]["Connection"][0]) === "close"))  fclose($state["fp"]);
				else  $state["result"]["fp"] = $state["fp"];

				return $state["result"];
			}
			else
			{
				return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("Invalid 'type' in state tracker."), "errorcode" => "invalid_type"));
			}
		}

		public static function RawFileSize($fileorname)
		{
			if (is_resource($fileorname))  $fp = $fileorname;
			else
			{
				$fp = @fopen($fileorname, "rb");
				if ($fp === false)  return 0;
			}

			if (PHP_INT_SIZE < 8)
			{
				$pos = 0;
				$size = 1073741824;
				fseek($fp, 0, SEEK_SET);
				while ($size > 1)
				{
					if (fseek($fp, $size, SEEK_CUR) === -1)  break;

					if (fgetc($fp) === false)
					{
						fseek($fp, -$size, SEEK_CUR);
						$size = (int)($size / 2);
					}
					else
					{
						fseek($fp, -1, SEEK_CUR);
						$pos += $size;
					}
				}

				if ($size > 1)
				{
					// Unfortunately, fseek() failed for some reason.  Going to have to do this the old-fashioned way.
					do
					{
						$data = fread($fp, 10485760);
						if ($data === false)  $data = "";
						$pos += strlen($data);
					} while ($data !== "");
				}
				else
				{
					while (fgetc($fp) !== false)  $pos++;
				}
			}
			else
			{
				fseek($fp, 0, SEEK_END);
				$pos = ftell($fp);
			}

			if (!is_resource($fileorname))  fclose($fp);

			return $pos;
		}

		public static function RetrieveWebpage($url, $options = array())
		{
			$startts = microtime(true);
			$timeout = (isset($options["timeout"]) ? $options["timeout"] : false);

			if (!function_exists("stream_socket_client") && !function_exists("fsockopen"))  return array("success" => false, "error" => self::HTTPTranslate("The functions 'stream_socket_client' and 'fsockopen' do not exist."), "errorcode" => "function_check");

			// Process the URL.
			$url = trim($url);
			$url = self::ExtractURL($url);

			if ($url["scheme"] != "http" && $url["scheme"] != "https")  return array("success" => false, "error" => self::HTTPTranslate("RetrieveWebpage() only supports the 'http' and 'https' protocols."), "errorcode" => "protocol_check");

			$secure = ($url["scheme"] == "https");
			$async = (isset($options["async"]) ? $options["async"] : false);
			$protocol = ($secure && !$async ? (isset($options["protocol"]) ? strtolower($options["protocol"]) : "ssl") : "tcp");
			if (function_exists("stream_get_transports") && !in_array($protocol, stream_get_transports()))  return array("success" => false, "error" => self::HTTPTranslate("The desired transport protocol '%s' is not installed.", $protocol), "errorcode" => "transport_not_installed");
			$host = str_replace(" ", "-", self::HeaderValueCleanup($url["host"]));
			if ($host == "")  return array("success" => false, "error" => self::HTTPTranslate("Invalid URL."));
			$port = ((int)$url["port"] ? (int)$url["port"] : ($secure ? 443 : 80));
			$defaultport = ((!$secure && $port == 80) || ($secure && $port == 443));
			$path = ($url["path"] == "" ? "/" : $url["path"]);
			$query = $url["query"];
			$username = $url["loginusername"];
			$password = $url["loginpassword"];

			// Cleanup input headers.
			if (!isset($options["headers"]))  $options["headers"] = array();
			$options["headers"] = self::NormalizeHeaders($options["headers"]);
			if (isset($options["rawheaders"]))  self::MergeRawHeaders($options["headers"], $options["rawheaders"]);

			// Process the proxy URL (if specified).
			$useproxy = (isset($options["proxyurl"]) && trim($options["proxyurl"]) != "");
			$proxysecure = false;
			$proxyconnect = false;
			$proxydata = "";
			if ($useproxy)
			{
				$proxyurl = trim($options["proxyurl"]);
				$proxyurl = self::ExtractURL($proxyurl);

				$proxysecure = ($proxyurl["scheme"] == "https");
				if ($proxysecure && $secure)  return array("success" => false, "error" => self::HTTPTranslate("The PHP SSL sockets implementation does not support tunneled SSL/TLS connections over SSL/TLS."), "errorcode" => "multi_ssl_tunneling_not_supported");
				$proxyprotocol = ($proxysecure && !$async ? (isset($options["proxyprotocol"]) ? strtolower($options["proxyprotocol"]) : "ssl") : "tcp");
				if (function_exists("stream_get_transports") && !in_array($proxyprotocol, stream_get_transports()))  return array("success" => false, "error" => self::HTTPTranslate("The desired transport proxy protocol '%s' is not installed.", $proxyprotocol), "errorcode" => "proxy_transport_not_installed");
				$proxyhost = str_replace(" ", "-", self::HeaderValueCleanup($proxyurl["host"]));
				if ($proxyhost === "")  return array("success" => false, "error" => self::HTTPTranslate("The specified proxy URL is not a URL.  Prefix 'proxyurl' with http:// or https://"), "errorcode" => "invalid_proxy_url");
				$proxyport = ((int)$proxyurl["port"] ? (int)$proxyurl["port"] : ($proxysecure ? 443 : 80));
				$proxypath = ($proxyurl["path"] == "" ? "/" : $proxyurl["path"]);
				$proxyusername = $proxyurl["loginusername"];
				$proxypassword = $proxyurl["loginpassword"];

				// Open a tunnel instead of letting the proxy modify the request (HTTP CONNECT).
				$proxyconnect = (isset($options["proxyconnect"]) && $options["proxyconnect"] ? $options["proxyconnect"] : false);
				if ($proxyconnect)
				{
					$proxydata = "CONNECT " . $host . ":" . $port . " HTTP/1.1\r\n";
					if (isset($options["headers"]["User-Agent"]))  $proxydata .= "User-Agent: " . $options["headers"]["User-Agent"] . "\r\n";
					$proxydata .= "Host: " . $host . ($defaultport ? "" : ":" . $port) . "\r\n";
					$proxydata .= "Proxy-Connection: keep-alive\r\n";
					if ($proxyusername != "")  $proxydata .= "Proxy-Authorization: BASIC " . base64_encode($proxyusername . ":" . $proxypassword) . "\r\n";
					if (!isset($options["proxyheaders"]))  $options["proxyheaders"] = array();
					$options["proxyheaders"] = self::NormalizeHeaders($options["proxyheaders"]);
					if (isset($options["rawproxyheaders"]))  self::MergeRawHeaders($options["proxyheaders"], $options["rawproxyheaders"]);

					unset($options["proxyheaders"]["Accept-Encoding"]);
					foreach ($options["proxyheaders"] as $name => $val)
					{
						if ($name != "Content-Type" && $name != "Content-Length" && $name != "Proxy-Connection" && $name != "Host")  $proxydata .= $name . ": " . $val . "\r\n";
					}

					$proxydata .= "\r\n";
					if (isset($options["debug_callback"]) && is_callable($options["debug_callback"]))  call_user_func_array($options["debug_callback"], array("rawproxyheaders", $proxydata, &$options["debug_callback_opts"]));
				}
			}

			// Process the method.
			if (!isset($options["method"]))
			{
				if ((isset($options["write_body_callback"]) && is_callable($options["write_body_callback"])) || isset($options["body"]))  $options["method"] = "PUT";
				else if (isset($options["postvars"]) || (isset($options["files"]) && count($options["files"])))  $options["method"] = "POST";
				else  $options["method"] = "GET";
			}
			$options["method"] = preg_replace('/[^A-Z]/', "", strtoupper($options["method"]));

			// Process the HTTP version.
			if (!isset($options["httpver"]))  $options["httpver"] = "1.1";
			$options["httpver"] = preg_replace('/[^0-9.]/', "", $options["httpver"]);

			// Process the request.
			$data = $options["method"] . " ";
			$data .= ($useproxy && !$proxyconnect ? $url["scheme"] . "://" . $host . ":" . $port : "") . $path . ($query != "" ? "?" . $query : "");
			$data .= " HTTP/" . $options["httpver"] . "\r\n";

			// Process the headers.
			if ($useproxy && !$proxyconnect && $proxyusername != "")  $data .= "Proxy-Authorization: BASIC " . base64_encode($proxyusername . ":" . $proxypassword) . "\r\n";
			if ($username != "")  $data .= "Authorization: BASIC " . base64_encode($username . ":" . $password) . "\r\n";
			$ver = explode(".", $options["httpver"]);
			if (isset($options["headers"]["Host"]))
			{
				$url2 = self::ExtractURL("http://" . $options["headers"]["Host"]);
				$options["headers"]["Host"] = $url2["host"] . (isset($url2["port"]) && $url2["port"] != "" ? ":" . $url2["port"] : "");
			}
			if ((int)$ver[0] > 1 || ((int)$ver[0] == 1 && (int)$ver[1] >= 1))
			{
				if (!isset($options["headers"]["Host"]))  $options["headers"]["Host"] = $host . ($defaultport ? "" : ":" . $port);
				$data .= "Host: " . $options["headers"]["Host"] . "\r\n";
			}

			if (!isset($options["headers"]["Connection"]))  $options["headers"]["Connection"] = "close";
			$data .= "Connection: " . $options["headers"]["Connection"] . "\r\n";

			foreach ($options["headers"] as $name => $val)
			{
				if ($name != "Content-Type" && $name != "Content-Length" && $name != "Connection" && $name != "Host")  $data .= $name . ": " . $val . "\r\n";
			}

			if (isset($options["files"]) && !count($options["files"]))  unset($options["files"]);

			// Process the body.
			$mime = "";
			$body = "";
			$bodysize = 0;
			if (isset($options["write_body_callback"]) && is_callable($options["write_body_callback"]))
			{
				if (isset($options["headers"]["Content-Type"]))  $data .= "Content-Type: " . $options["headers"]["Content-Type"] . "\r\n";

				call_user_func_array($options["write_body_callback"], array(&$body, &$bodysize, &$options["write_body_callback_opts"]));
			}
			else if (isset($options["body"]))
			{
				if (isset($options["headers"]["Content-Type"]))  $data .= "Content-Type: " . $options["headers"]["Content-Type"] . "\r\n";

				$body = $options["body"];
				$bodysize = strlen($body);
				unset($options["body"]);
			}
			else if ((isset($options["files"]) && count($options["files"])) || (isset($options["headers"]["Content-Type"]) && stripos($options["headers"]["Content-Type"], "multipart/form-data") !== false))
			{
				$mime = "--------" . substr(sha1(uniqid(mt_rand(), true)), 0, 25);
				$data .= "Content-Type: multipart/form-data; boundary=" . $mime . "\r\n";
				if (isset($options["postvars"]))
				{
					foreach ($options["postvars"] as $name => $val)
					{
						$name = self::HeaderValueCleanup($name);
						$name = str_replace("\"", "", $name);

						if (!is_array($val))
						{
							if (is_string($val) || is_numeric($val))  $val = array($val);
							else  return array("success" => false, "error" => "A supplied 'postvars' value is an invalid type.  Expected string, numeric, or array.", "errorcode" => "invalid_postvars_value", "info" => array("name" => $name, "val" => $val));
						}

						foreach ($val as $val2)
						{
							$body .= "--" . $mime . "\r\n";
							$body .= "Content-Disposition: form-data; name=\"" . $name . "\"\r\n";
							$body .= "\r\n";
							$body .= $val2 . "\r\n";
						}
					}

					unset($options["postvars"]);
				}

				$bodysize = strlen($body);

				// Only count the amount of data to send.
				if (!isset($options["files"]))  $options["files"] = array();
				foreach ($options["files"] as $num => $info)
				{
					$name = self::HeaderValueCleanup($info["name"]);
					$name = str_replace("\"", "", $name);
					$filename = self::FilenameSafe(self::ExtractFilename($info["filename"]));
					$type = self::HeaderValueCleanup($info["type"]);

					$body2 = "--" . $mime . "\r\n";
					$body2 .= "Content-Disposition: form-data; name=\"" . $name . "\"; filename=\"" . $filename . "\"\r\n";
					$body2 .= "Content-Type: " . $type . "\r\n";
					$body2 .= "\r\n";

					$info["filesize"] = (isset($info["datafile"]) ? self::RawFileSize($info["datafile"]) : strlen($info["data"]));
					$bodysize += strlen($body2) + $info["filesize"] + 2;

					$options["files"][$num] = $info;
				}

				$body2 = "--" . $mime . "--\r\n";
				$bodysize += strlen($body2);
			}
			else
			{
				if (isset($options["postvars"]))
				{
					foreach ($options["postvars"] as $name => $val)
					{
						$name = self::HeaderValueCleanup($name);

						if (!is_array($val))
						{
							if (is_string($val) || is_numeric($val))  $val = array($val);
							else  return array("success" => false, "error" => "A supplied 'postvars' value is an invalid type.  Expected string, numeric, or array.", "errorcode" => "invalid_postvars_value", "info" => array("name" => $name, "val" => $val));
						}

						foreach ($val as $val2)  $body .= ($body != "" ? "&" : "") . urlencode($name) . "=" . urlencode($val2);
					}

					unset($options["postvars"]);
				}

				if ($body != "")  $data .= "Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n";

				$bodysize = strlen($body);
			}
			if (($bodysize === false && strlen($body) > 0) || ($bodysize !== false && $bodysize < strlen($body)))  $bodysize = strlen($body);

			// Finalize the headers.
			if ($bodysize === false)  $data .= "Transfer-Encoding: chunked\r\n";
			else if ($bodysize > 0 || $body != "" || $options["method"] == "POST")  $data .= "Content-Length: " . $bodysize . "\r\n";
			$data .= "\r\n";
			if (isset($options["debug_callback"]) && is_callable($options["debug_callback"]))  call_user_func_array($options["debug_callback"], array("rawheaders", $data, &$options["debug_callback_opts"]));
			$rawheadersize = strlen($data);

			// Finalize the initial data to be sent.
			$data .= $body;
			if ($bodysize !== false)  $bodysize -= strlen($body);
			$body = "";
			$result = array("success" => true, "rawsendsize" => 0, "rawsendheadersize" => $rawheadersize, "rawrecvsize" => 0, "rawrecvheadersize" => 0, "startts" => $startts);
			$debug = (isset($options["debug"]) && $options["debug"]);
			if ($debug)
			{
				$result["rawsend"] = "";
				$result["rawrecv"] = "";
			}

			if ($timeout !== false && self::GetTimeLeft($startts, $timeout) == 0)  return array("success" => false, "error" => self::HTTPTranslate("HTTP timeout exceeded."), "errorcode" => "timeout_exceeded");

			// Connect to the target server.
			$errornum = 0;
			$errorstr = "";
			if (isset($options["fp"]) && is_resource($options["fp"]))
			{
				$fp = $options["fp"];
				unset($options["fp"]);

				$useproxy = false;
				$proxyconnect = false;
				$proxydata = "";
			}
			else if ($useproxy)
			{
				if (!isset($options["proxyconnecttimeout"]))  $options["proxyconnecttimeout"] = 10;
				$timeleft = self::GetTimeLeft($startts, $timeout);
				if ($timeleft !== false)  $options["proxyconnecttimeout"] = min($options["proxyconnecttimeout"], $timeleft);
				if (!function_exists("stream_socket_client"))
				{
					if ($debug)  $fp = fsockopen($proxyprotocol . "://" . $proxyhost, $proxyport, $errornum, $errorstr, $options["proxyconnecttimeout"]);
					else  $fp = @fsockopen($proxyprotocol . "://" . $proxyhost, $proxyport, $errornum, $errorstr, $options["proxyconnecttimeout"]);
				}
				else
				{
					$context = @stream_context_create();
					if (isset($options["source_ip"]))  $context["socket"] = array("bindto" => $options["source_ip"] . ":0");
					if ($proxysecure)
					{
						if (!isset($options["proxysslopts"]) || !is_array($options["proxysslopts"]))  $options["proxysslopts"] = self::GetSafeSSLOpts();
						self::ProcessSSLOptions($options, "proxysslopts", $host);
						foreach ($options["proxysslopts"] as $key => $val)  @stream_context_set_option($context, "ssl", $key, $val);
					}
					else if ($secure)
					{
						if (!isset($options["sslopts"]) || !is_array($options["sslopts"]))
						{
							$options["sslopts"] = self::GetSafeSSLOpts();
							$options["sslopts"]["auto_peer_name"] = true;
						}

						self::ProcessSSLOptions($options, "sslopts", $host);
						foreach ($options["sslopts"] as $key => $val)  @stream_context_set_option($context, "ssl", $key, $val);
					}

					if ($debug)  $fp = stream_socket_client($proxyprotocol . "://" . $proxyhost . ":" . $proxyport, $errornum, $errorstr, $options["proxyconnecttimeout"], ($async ? STREAM_CLIENT_ASYNC_CONNECT : STREAM_CLIENT_CONNECT), $context);
					else  $fp = @stream_socket_client($proxyprotocol . "://" . $proxyhost . ":" . $proxyport, $errornum, $errorstr, $options["proxyconnecttimeout"], ($async ? STREAM_CLIENT_ASYNC_CONNECT : STREAM_CLIENT_CONNECT), $context);
				}

				if ($fp === false)  return array("success" => false, "error" => self::HTTPTranslate("Unable to establish a connection to '%s'.", ($proxysecure ? $proxyprotocol . "://" : "") . $proxyhost . ":" . $proxyport), "info" => $errorstr . " (" . $errornum . ")", "errorcode" => "proxy_connect");
			}
			else
			{
				if (!isset($options["connecttimeout"]))  $options["connecttimeout"] = 10;
				$timeleft = self::GetTimeLeft($startts, $timeout);
				if ($timeleft !== false)  $options["connecttimeout"] = min($options["connecttimeout"], $timeleft);
				if (!function_exists("stream_socket_client"))
				{
					if ($debug)  $fp = fsockopen($protocol . "://" . $host, $port, $errornum, $errorstr, $options["connecttimeout"]);
					else  $fp = @fsockopen($protocol . "://" . $host, $port, $errornum, $errorstr, $options["connecttimeout"]);
				}
				else
				{
					$context = @stream_context_create();
					if (isset($options["source_ip"]))  $context["socket"] = array("bindto" => $options["source_ip"] . ":0");
					if ($secure)
					{
						if (!isset($options["sslopts"]) || !is_array($options["sslopts"]))
						{
							$options["sslopts"] = self::GetSafeSSLOpts();
							$options["sslopts"]["auto_peer_name"] = true;
						}

						self::ProcessSSLOptions($options, "sslopts", $host);
						foreach ($options["sslopts"] as $key => $val)  @stream_context_set_option($context, "ssl", $key, $val);
					}

					if ($debug)  $fp = stream_socket_client($protocol . "://" . $host . ":" . $port, $errornum, $errorstr, $options["connecttimeout"], ($async ? STREAM_CLIENT_ASYNC_CONNECT : STREAM_CLIENT_CONNECT), $context);
					else $fp = @stream_socket_client($protocol . "://" . $host . ":" . $port, $errornum, $errorstr, $options["connecttimeout"], ($async ? STREAM_CLIENT_ASYNC_CONNECT : STREAM_CLIENT_CONNECT), $context);
				}

				if ($fp === false)  return array("success" => false, "error" => self::HTTPTranslate("Unable to establish a connection to '%s'.", ($secure ? $protocol . "://" : "") . $host . ":" . $port), "info" => $errorstr . " (" . $errornum . ")", "errorcode" => "connect_failed");
			}

			if (function_exists("stream_set_blocking"))  @stream_set_blocking($fp, ($async ? 0 : 1));

			// Initialize the connection request state array.
			$state = array(
				"fp" => $fp,
				"type" => "request",
				"async" => $async,
				"debug" => $debug,
				"startts" => $startts,
				"timeout" => $timeout,
				"waituntil" => -1.0,
				"mime" => $mime,
				"data" => $data,
				"bodysize" => $bodysize,
				"chunked" => ($bodysize === false),
				"secure" => $secure,
				"useproxy" => $useproxy,
				"proxysecure" => $proxysecure,
				"proxyconnect" => $proxyconnect,
				"proxydata" => $proxydata,
				"currentfile" => false,

				"state" => "connecting",

				"options" => $options,
				"result" => $result,
				"close" => ($options["headers"]["Connection"] === "close"),
				"nextread" => "",
				"client" => true
			);

			// Return the state for async calls.  Caller must call ProcessState().
			if ($state["async"])  return array("success" => true, "state" => $state);

			// Run through all of the valid states and return the result.
			return self::ProcessState($state);
		}
	}
?>