├── README.md ├── support ├── crc32_stream.php ├── multi_async_helper.php ├── deflate_stream.php ├── cli.php ├── utf_utils.php ├── generic_server.php ├── web_browser.php └── http.php └── net-test.php /README.md: -------------------------------------------------------------------------------- 1 | Command-line Network Testing Tool 2 | ================================= 3 | 4 | This is a very simple tool designed to quickly set up a debuggable TCP/IP echo server or client. The server waits for a connection and the client will keep retrying until it successfully connects. Designed for diagnosing network connectivity issues. 5 | 6 | Features 7 | -------- 8 | 9 | * Simple TCP/IP echo server/client with optional SSL/TLS support. 10 | * A complete, question/answer enabled command-line interface. Nothing to compile. 11 | * GenericServer class. Very noisy when debug mode is turned on (a good thing in this case). 12 | * Website connectivity tester. Useful for monitoring and tracking down extremely difficult/unusual initial connection issues. 13 | * Also has a liberal open source license. MIT or LGPL, your choice. 14 | * Designed for relatively painless integration into your environment. 15 | * Sits on GitHub for all of that pull request and issue tracker goodness to easily submit changes and ideas respectively. 16 | 17 | Getting Started 18 | --------------- 19 | 20 | The easiest way to get started is to play with the command-line interface. The command-line interface is question/answer enabled, which means all you have to do is run: 21 | 22 | ```` 23 | php net-test.php 24 | ```` 25 | 26 | Once you grow tired of manually entering information, you can pass in some or all of the answers to the questions on the command-line: 27 | 28 | ```` 29 | php net-test.php server bind= port=12345 ssl=N 30 | 31 | php net-test.php -s client bind= host=127.0.0.1 port=12345 ssl=N retry= msg="It works!" 32 | ```` 33 | 34 | The -s option suppresses entry output. 35 | -------------------------------------------------------------------------------- /support/crc32_stream.php: -------------------------------------------------------------------------------- 1 | 0x04C11DB7, "start" => 0xFFFFFFFF, "xor" => 0xFFFFFFFF, "refdata" => 1, "refcrc" => 1); 14 | 15 | public function __construct() 16 | { 17 | $this->open = false; 18 | } 19 | 20 | public function Init($options = false) 21 | { 22 | if ($options === false && function_exists("hash_init")) $this->hash = hash_init("crc32b"); 23 | else 24 | { 25 | if ($options === false) $options = self::$default; 26 | 27 | $this->hash = false; 28 | $this->crctable = array(); 29 | $poly = $this->LIM32($options["poly"]); 30 | for ($x = 0; $x < 256; $x++) 31 | { 32 | $c = $this->SHL32($x, 24); 33 | for ($y = 0; $y < 8; $y++) $c = $this->SHL32($c, 1) ^ ($c & 0x80000000 ? $poly : 0); 34 | $this->crctable[$x] = $c; 35 | } 36 | 37 | $this->datareflect = $options["refdata"]; 38 | $this->crcreflect = $options["refcrc"]; 39 | $this->firstcrc = $options["start"]; 40 | $this->currcrc = $options["start"]; 41 | $this->finalxor = $options["xor"]; 42 | } 43 | 44 | $this->open = true; 45 | } 46 | 47 | public function AddData($data) 48 | { 49 | if (!$this->open) return false; 50 | 51 | if ($this->hash !== false) hash_update($this->hash, $data); 52 | else 53 | { 54 | $y = strlen($data); 55 | 56 | for ($x = 0; $x < $y; $x++) 57 | { 58 | if ($this->datareflect) $this->currcrc = $this->SHL32($this->currcrc, 8) ^ $this->crctable[$this->SHR32($this->currcrc, 24) ^ self::$revlookup[ord($data[$x])]]; 59 | else $this->currcrc = $this->SHL32($this->currcrc, 8) ^ $this->crctable[$this->SHR32($this->currcrc, 24) ^ ord($data[$x])]; 60 | } 61 | } 62 | 63 | return true; 64 | } 65 | 66 | public function Finalize() 67 | { 68 | if (!$this->open) return false; 69 | 70 | if ($this->hash !== false) 71 | { 72 | $result = hexdec(hash_final($this->hash)); 73 | 74 | $this->hash = hash_init("crc32b"); 75 | } 76 | else 77 | { 78 | if ($this->crcreflect) 79 | { 80 | $tempcrc = $this->currcrc; 81 | $this->currcrc = self::$revlookup[$this->SHR32($tempcrc, 24)] | $this->SHL32(self::$revlookup[$this->SHR32($tempcrc, 16) & 0xFF], 8) | $this->SHL32(self::$revlookup[$this->SHR32($tempcrc, 8) & 0xFF], 16) | $this->SHL32(self::$revlookup[$this->LIM32($tempcrc & 0xFF)], 24); 82 | } 83 | $result = $this->currcrc ^ $this->finalxor; 84 | 85 | $this->currcrc = $this->firstcrc; 86 | } 87 | 88 | return $result; 89 | } 90 | 91 | // These functions are a hacky, but effective way of enforcing unsigned 32-bit integers onto a generic signed int. 92 | // Allow bitwise operations to work across platforms. Minimum integer size must be 32-bit. 93 | private function SHR32($num, $bits) 94 | { 95 | $num = (int)$num; 96 | if ($bits < 0) $bits = 0; 97 | 98 | if ($num < 0 && $bits) 99 | { 100 | $num = ($num >> 1) & 0x7FFFFFFF; 101 | $bits--; 102 | } 103 | 104 | return $this->LIM32($num >> $bits); 105 | } 106 | 107 | private function SHL32($num, $bits) 108 | { 109 | if ($bits < 0) $bits = 0; 110 | 111 | return $this->LIM32((int)$num << $bits); 112 | } 113 | 114 | private function LIM32($num) 115 | { 116 | return (int)((int)$num & 0xFFFFFFFF); 117 | } 118 | } 119 | ?> -------------------------------------------------------------------------------- /support/multi_async_helper.php: -------------------------------------------------------------------------------- 1 | objs = array(); 12 | $this->queuedobjs = array(); 13 | $this->limit = false; 14 | } 15 | 16 | public function SetConcurrencyLimit($limit) 17 | { 18 | $this->limit = $limit; 19 | } 20 | 21 | public function Set($key, $obj, $callback) 22 | { 23 | if (is_callable($callback)) 24 | { 25 | $this->queuedobjs[$key] = array( 26 | "obj" => $obj, 27 | "callback" => $callback 28 | ); 29 | 30 | unset($this->objs[$key]); 31 | } 32 | } 33 | 34 | public function NumObjects() 35 | { 36 | return count($this->queuedobjs) + count($this->objs); 37 | } 38 | 39 | public function GetObject($key) 40 | { 41 | if (isset($this->queuedobjs[$key])) $result = $this->queuedobjs[$key]["obj"]; 42 | else if (isset($this->objs[$key])) $result = $this->objs[$key]["obj"]; 43 | else $result = false; 44 | 45 | return $result; 46 | } 47 | 48 | // To be able to change a callback on the fly. 49 | public function SetCallback($key, $callback) 50 | { 51 | if (is_callable($callback)) 52 | { 53 | if (isset($this->queuedobjs[$key])) $this->queuedobjs[$key]["callback"] = $callback; 54 | else if (isset($this->objs[$key])) $this->objs[$key]["callback"] = $callback; 55 | } 56 | } 57 | 58 | private function InternalDetach($key, $cleanup) 59 | { 60 | if (isset($this->queuedobjs[$key])) 61 | { 62 | call_user_func_array($this->queuedobjs[$key]["callback"], array("cleanup", &$cleanup, $key, &$this->queuedobjs[$key]["obj"])); 63 | $result = $this->queuedobjs[$key]["obj"]; 64 | unset($this->queuedobjs[$key]); 65 | } 66 | else if (isset($this->objs[$key])) 67 | { 68 | call_user_func_array($this->objs[$key]["callback"], array("cleanup", &$cleanup, $key, &$this->objs[$key]["obj"])); 69 | $result = $this->objs[$key]["obj"]; 70 | unset($this->objs[$key]); 71 | } 72 | else 73 | { 74 | $result = false; 75 | } 76 | 77 | return $result; 78 | } 79 | 80 | public function Detach($key) 81 | { 82 | return $this->InternalDetach($key, false); 83 | } 84 | 85 | public function Remove($key) 86 | { 87 | return $this->InternalDetach($key, true); 88 | } 89 | 90 | // A few default functions for direct file/socket handles. 91 | public static function ReadOnly($mode, &$data, $key, $fp) 92 | { 93 | switch ($mode) 94 | { 95 | case "init": 96 | case "update": 97 | { 98 | // Move to/Keep in the live queue. 99 | if (is_resource($fp)) $data = true; 100 | 101 | break; 102 | } 103 | case "read": 104 | case "write": 105 | case "writefps": 106 | { 107 | break; 108 | } 109 | case "readfps": 110 | { 111 | $data[$key] = $fp; 112 | 113 | break; 114 | } 115 | case "cleanup": 116 | { 117 | if ($data === true) @fclose($fp); 118 | 119 | break; 120 | } 121 | } 122 | } 123 | 124 | public static function WriteOnly($mode, &$data, $key, $fp) 125 | { 126 | switch ($mode) 127 | { 128 | case "init": 129 | case "update": 130 | { 131 | // Move to/Keep in the live queue. 132 | if (is_resource($fp)) $data = true; 133 | 134 | break; 135 | } 136 | case "read": 137 | case "readfps": 138 | case "write": 139 | { 140 | break; 141 | } 142 | case "writefps": 143 | { 144 | $data[$key] = $fp; 145 | 146 | break; 147 | } 148 | case "cleanup": 149 | { 150 | if ($data === true) @fclose($fp); 151 | 152 | break; 153 | } 154 | } 155 | } 156 | 157 | public static function ReadAndWrite($mode, &$data, $key, $fp) 158 | { 159 | switch ($mode) 160 | { 161 | case "init": 162 | case "update": 163 | { 164 | // Move to/Keep in the live queue. 165 | if (is_resource($fp)) $data = true; 166 | 167 | break; 168 | } 169 | case "read": 170 | case "write": 171 | { 172 | break; 173 | } 174 | case "readfps": 175 | case "writefps": 176 | { 177 | $data[$key] = $fp; 178 | 179 | break; 180 | } 181 | case "cleanup": 182 | { 183 | if ($data === true) @fclose($fp); 184 | 185 | break; 186 | } 187 | } 188 | } 189 | 190 | public function Wait($timeout = false) 191 | { 192 | // Move queued objects to live. 193 | $result2 = array("success" => true, "read" => array(), "write" => array(), "removed" => array(), "new" => array()); 194 | while (count($this->queuedobjs) && ($this->limit === false || count($this->objs) < $this->limit)) 195 | { 196 | $info = reset($this->queuedobjs); 197 | $key = key($this->queuedobjs); 198 | unset($this->queuedobjs[$key]); 199 | 200 | $result2["new"][$key] = $key; 201 | 202 | $keep = false; 203 | call_user_func_array($info["callback"], array("init", &$keep, $key, &$info["obj"])); 204 | 205 | $this->objs[$key] = $info; 206 | 207 | if (!$keep) $result2["removed"][$key] = $this->Remove($key); 208 | } 209 | 210 | // Walk the objects looking for read and write handles. 211 | $readfps = array(); 212 | $writefps = array(); 213 | $exceptfps = NULL; 214 | foreach ($this->objs as $key => &$info) 215 | { 216 | $keep = false; 217 | call_user_func_array($info["callback"], array("update", &$keep, $key, &$info["obj"])); 218 | 219 | if (!$keep) $result2["removed"][$key] = $this->Remove($key); 220 | else 221 | { 222 | call_user_func_array($info["callback"], array("readfps", &$readfps, $key, &$info["obj"])); 223 | call_user_func_array($info["callback"], array("writefps", &$writefps, $key, &$info["obj"])); 224 | } 225 | } 226 | if (!count($readfps)) $readfps = NULL; 227 | if (!count($writefps)) $writefps = NULL; 228 | 229 | // Wait for something to happen. 230 | if (isset($readfps) || isset($writefps)) 231 | { 232 | if ($timeout === false) $timeout = NULL; 233 | $readfps2 = $readfps; 234 | $writefps2 = $writefps; 235 | $result = @stream_select($readfps, $writefps, $exceptfps, $timeout); 236 | if ($result === false) return array("success" => false, "error" => self::MAHTranslate("Wait() failed due to stream_select() failure. Most likely cause: Connection failure."), "errorcode" => "stream_select_failed"); 237 | else if ($result > 0) 238 | { 239 | if (isset($readfps)) 240 | { 241 | $readfps3 = array(); 242 | foreach ($readfps as $key => $fp) 243 | { 244 | if (!isset($readfps2[$key]) || $readfps2[$key] !== $fp) 245 | { 246 | foreach ($readfps2 as $key2 => $fp2) 247 | { 248 | if ($fp === $fp2) $key = $key2; 249 | } 250 | } 251 | 252 | if (isset($this->objs[$key])) 253 | { 254 | call_user_func_array($this->objs[$key]["callback"], array("read", &$fp, $key, &$this->objs[$key]["obj"])); 255 | 256 | $readfps3[$key] = $fp; 257 | } 258 | } 259 | 260 | $result2["read"] = $readfps3; 261 | } 262 | 263 | if (isset($writefps)) 264 | { 265 | $writefps3 = array(); 266 | foreach ($writefps as $key => $fp) 267 | { 268 | if (!isset($writefps2[$key]) || $writefps2[$key] !== $fp) 269 | { 270 | foreach ($writefps2 as $key2 => $fp2) 271 | { 272 | if ($fp === $fp2) $key = $key2; 273 | } 274 | } 275 | 276 | if (isset($this->objs[$key])) 277 | { 278 | call_user_func_array($this->objs[$key]["callback"], array("write", &$fp, $key, &$this->objs[$key]["obj"])); 279 | 280 | $readfps3[$key] = $fp; 281 | } 282 | } 283 | 284 | $result2["write"] = $writefps3; 285 | } 286 | } 287 | } 288 | 289 | $result2["numleft"] = count($this->queuedobjs) + count($this->objs); 290 | 291 | return $result2; 292 | } 293 | 294 | public static function MAHTranslate() 295 | { 296 | $args = func_get_args(); 297 | if (!count($args)) return ""; 298 | 299 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 300 | } 301 | } 302 | ?> -------------------------------------------------------------------------------- /net-test.php: -------------------------------------------------------------------------------- 1 | array( 21 | "s" => "suppressoutput", 22 | "?" => "help" 23 | ), 24 | "rules" => array( 25 | "suppressoutput" => array("arg" => false), 26 | "help" => array("arg" => false) 27 | ) 28 | ); 29 | $args = CLI::ParseCommandLine($options); 30 | 31 | if (isset($args["opts"]["help"])) 32 | { 33 | echo "Network server/client command-line tools\n"; 34 | echo "Purpose: Start a test echo server or client directly from the command-line.\n"; 35 | echo "\n"; 36 | echo "This tool is question/answer enabled. Just running it will provide a guided interface. It can also be run entirely from the command-line if you know all the answers.\n"; 37 | echo "\n"; 38 | echo "Syntax: " . $args["file"] . " [options] [cmd [cmdoptions]]\n"; 39 | echo "Options:\n"; 40 | echo "\t-s Suppress entry output.\n"; 41 | echo "\n"; 42 | echo "Examples:\n"; 43 | echo "\tphp " . $args["file"] . "\n"; 44 | echo "\tphp " . $args["file"] . " server bind= port=12345 ssl=N\n"; 45 | echo "\tphp " . $args["file"] . " client bind= host=127.0.0.1 port=12345 ssl=N retry= msg=\"It works!\"\n"; 46 | 47 | exit(); 48 | } 49 | 50 | $suppressoutput = (isset($args["opts"]["suppressoutput"]) && $args["opts"]["suppressoutput"]); 51 | 52 | // Get the command group. 53 | $cmds = array( 54 | "server" => "Start an echo server", 55 | "client" => "Connect to an echo server", 56 | "web-tcp" => "Repeatedly retrieve a single URL" 57 | ); 58 | 59 | $cmd = CLI::GetLimitedUserInputWithArgs($args, false, "Command", false, "Available commands:", $cmds, true, $suppressoutput); 60 | 61 | function DisplayResult($result) 62 | { 63 | echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; 64 | 65 | exit(); 66 | } 67 | 68 | if ($cmd === "server") 69 | { 70 | CLI::ReinitArgs($args, array("bind", "port", "ssl", "cert", "key")); 71 | 72 | $bind = CLI::GetUserInputWithArgs($args, "bind", "Bind to IP", "0.0.0.0", "The bind to IP address is usually 0.0.0.0 or 127.0.0.1 for IPv4 and [::0] or [::1] for IPv6.", $suppressoutput); 73 | $port = (int)CLI::GetUserInputWithArgs($args, "port", "Port", false, "", $suppressoutput); 74 | $ssl = CLI::GetYesNoUserInputWithArgs($args, "ssl", "SSL", "N", "", $suppressoutput); 75 | 76 | if ($ssl) 77 | { 78 | $certfile = CLI::GetUserInputWithArgs($args, "cert", "SSL certificate chain", false, "A SSL certificate chain file consists of the server certificate and CA intermediates.", $suppressoutput); 79 | $keyfile = CLI::GetUserInputWithArgs($args, "key", "SSL private key", false, "The private key file contains the private key for the server certificate.", $suppressoutput); 80 | 81 | $sslopts = array( 82 | "local_cert" => $certfile, 83 | "local_pk" => $keyfile 84 | ); 85 | } 86 | 87 | echo "Starting server...\n"; 88 | $es = new GenericServer(); 89 | $es->SetDebug(true); 90 | $result = $es->Start($bind, $port, ($ssl ? $sslopts : false)); 91 | if (!$result["success"]) DisplayResult($result); 92 | 93 | echo "Ready.\n"; 94 | 95 | $tracker = array(); 96 | 97 | do 98 | { 99 | $result = $es->Wait(); 100 | if (!$result["success"]) break; 101 | 102 | // Handle active clients. 103 | foreach ($result["clients"] as $id => $client) 104 | { 105 | if (!isset($tracker[$id])) 106 | { 107 | echo "Client " . $id . " connected.\n"; 108 | 109 | $tracker[$id] = array(); 110 | } 111 | 112 | if ($client->readdata != "") 113 | { 114 | echo "Client " . $id . " received: " . $client->readdata . "\n"; 115 | 116 | $client->writedata .= $client->readdata; 117 | $client->readdata = ""; 118 | } 119 | } 120 | 121 | // Do something with removed clients. 122 | foreach ($result["removed"] as $id => $result2) 123 | { 124 | if (isset($tracker[$id])) 125 | { 126 | echo "Client " . $id . " disconnected.\n"; 127 | 128 | echo "Client " . $id . " disconnected. " . $result2["client"]->recvsize . " bytes received, " . $result2["client"]->sendsize . " bytes sent. Disconnect reason:\n"; 129 | echo json_encode($result2["result"], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 130 | echo "\n"; 131 | 132 | unset($tracker[$id]); 133 | } 134 | } 135 | } while(1); 136 | } 137 | else if ($cmd === "client") 138 | { 139 | CLI::ReinitArgs($args, array("bind", "host", "port", "ssl", "retry", "msg")); 140 | 141 | $bind = CLI::GetUserInputWithArgs($args, "bind", "Bind to IP", "", "Binding to a specific IP controls the interface that packets will be sent out on. Leave blank for the default interface.", $suppressoutput); 142 | $host = CLI::GetUserInputWithArgs($args, "host", "Host", false, "", $suppressoutput); 143 | $port = (int)CLI::GetUserInputWithArgs($args, "port", "Port", false, "", $suppressoutput); 144 | $ssl = CLI::GetYesNoUserInputWithArgs($args, "ssl", "SSL", "N", "", $suppressoutput); 145 | $retry = (int)CLI::GetUserInputWithArgs($args, "retry", "Retry", "-1", "", $suppressoutput); 146 | $msg = CLI::GetUserInputWithArgs($args, "msg", "Message", false, "", $suppressoutput); 147 | 148 | function GetSafeSSLOpts($cafile = true, $cipherstype = "intermediate") 149 | { 150 | // Result array last updated May 3, 2017. 151 | $result = array( 152 | "ciphers" => GenericServer::GetSSLCiphers($cipherstype), 153 | "disable_compression" => true, 154 | "allow_self_signed" => false, 155 | "verify_peer" => true, 156 | "verify_depth" => 5 157 | ); 158 | 159 | if ($cafile === true) $result["auto_cainfo"] = true; 160 | else if ($cafile !== false) $result["cafile"] = $cafile; 161 | 162 | if (isset($result["auto_cainfo"])) 163 | { 164 | unset($result["auto_cainfo"]); 165 | 166 | $cainfo = ini_get("curl.cainfo"); 167 | if ($cainfo !== false && strlen($cainfo) > 0) $result["cafile"] = $cainfo; 168 | else if (file_exists(str_replace("\\", "/", dirname(__FILE__)) . "/support/cacert.pem")) $result["cafile"] = str_replace("\\", "/", dirname(__FILE__)) . "/support/cacert.pem"; 169 | } 170 | 171 | return $result; 172 | } 173 | 174 | $context = stream_context_create(); 175 | if ($bind !== "") $context["socket"] = array("bindto" => $bind . ":0"); 176 | if ($ssl) 177 | { 178 | $protocol = "ssl"; 179 | 180 | $sslopts = GetSafeSSLOpts(); 181 | foreach ($sslopts as $key => $val) stream_context_set_option($context, "ssl", $key, $val); 182 | } 183 | else 184 | { 185 | $protocol = "tcp"; 186 | } 187 | 188 | // Connect to the host. 189 | echo "\n"; 190 | echo "Connecting to " . $protocol . "://" . $host . ":" . $port . "...\n"; 191 | do 192 | { 193 | $fp = stream_socket_client($protocol . "://" . $host . ":" . $port, $errornum, $errorstr, 3, STREAM_CLIENT_CONNECT, $context); 194 | if ($fp !== false) break; 195 | 196 | if ($retry < 0) continue; 197 | else if ($retry > 1) 198 | { 199 | $retry--; 200 | echo "Connection attempt failed. Retries left: " . $retry . "\n"; 201 | 202 | continue; 203 | } 204 | else 205 | { 206 | echo "Connection attempt failed.\n"; 207 | 208 | exit(); 209 | } 210 | } while (1); 211 | 212 | // Send the message. 213 | for ($x = 0; $x < 3; $x++) 214 | { 215 | echo "Sending '" . $msg . "'...\n"; 216 | fwrite($fp, $msg); 217 | 218 | $msg2 = fread($fp, strlen($msg)); 219 | echo "Received '" . $msg2 . "'.\n"; 220 | 221 | usleep(250000); 222 | } 223 | 224 | // Close the connection. 225 | echo "Closing connection.\n"; 226 | fclose($fp); 227 | } 228 | else if ($cmd === "web-tcp") 229 | { 230 | CLI::ReinitArgs($args, array("bind", "url", "frequency", "rawfile")); 231 | 232 | $bind = CLI::GetUserInputWithArgs($args, "bind", "Bind to IP", "", "Binding to a specific IP controls the interface that packets will be sent out on. Leave blank for the default interface.", $suppressoutput); 233 | $url = CLI::GetUserInputWithArgs($args, "url", "HTTP(S) URL", false, "Ideally should be a URL to a web server you control and the URL only returns a few bytes of data. GET request only.", $suppressoutput); 234 | $freq = (int)CLI::GetUserInputWithArgs($args, "frequency", "Request frequency (seconds)", "1", "", $suppressoutput); 235 | if ($freq < 1) $freq = 1; 236 | $rawfile = CLI::GetUserInputWithArgs($args, "rawfile", "Raw CSV file", "", "Where to store output for later analysis. Useful for tracing difficult connectivity issues.", $suppressoutput); 237 | 238 | if ($rawfile === "") $fp = false; 239 | else 240 | { 241 | $init = (!file_exists($rawfile)); 242 | $fp = fopen($rawfile, ($init ? "wb" : "ab")); 243 | if ($init) fputcsv($fp, array("timestamp", "url", "active_reqs", "result", "bytes_sent", "bytes_recv", "total_time", "req_time", "response_time")); 244 | } 245 | 246 | require_once $rootpath . "/support/web_browser.php"; 247 | require_once $rootpath . "/support/multi_async_helper.php"; 248 | 249 | $nextts = microtime(true); 250 | 251 | // Use a multi-async helper to maintain request frequency consistency. 252 | $pages = array(); 253 | $helper = new MultiAsyncHelper(); 254 | 255 | while (1) 256 | { 257 | $ts = microtime(true); 258 | if ($ts >= $nextts) 259 | { 260 | $options = array(); 261 | if ($bind !== "") $options["source_ip"] = $bind; 262 | 263 | $pages[(int)$ts] = array("ts" => $ts, "web" => new WebBrowser()); 264 | $pages[(int)$ts]["web"]->ProcessAsync($helper, (int)$ts, NULL, $url, $options); 265 | 266 | $nextts += $freq; 267 | } 268 | 269 | $result = $helper->Wait(0); 270 | if (!$result["success"]) 271 | { 272 | var_dump($result); 273 | 274 | exit(); 275 | } 276 | 277 | // Process finished pages. 278 | foreach ($result["removed"] as $ts2 => $info) 279 | { 280 | echo "[" . date("Y-m-d H:i:s", $ts2) . "] "; 281 | 282 | if (!$info["result"]["success"]) 283 | { 284 | echo $info["result"]["error"] . " (" . $info["result"]["errorcode"] . ")\n"; 285 | 286 | if ($fp !== false) 287 | { 288 | fputcsv($fp, array(date("Y-m-d H:i:s", $ts2), $url, count($pages), $info["result"]["error"] . " (" . $info["result"]["errorcode"] . ")", "0", "0", $ts - $pages[$ts2]["ts"], "0", "0")); 289 | fflush($fp); 290 | } 291 | } 292 | else 293 | { 294 | if ($info["result"]["response"]["code"] != 200) echo $info["result"]["response"]["line"] . "\n"; 295 | else echo "OK\n"; 296 | 297 | if ($fp !== false) 298 | { 299 | fputcsv($fp, array(date("Y-m-d H:i:s", $ts2), $url, count($pages), $info["result"]["response"]["line"], $info["result"]["rawsendsize"], $info["result"]["rawrecvsize"], $info["result"]["endts"] - $info["result"]["startts"], $info["result"]["recvstart"] - $info["result"]["sendstart"], $info["result"]["endts"] - $info["result"]["recvstart"])); 300 | fflush($fp); 301 | } 302 | } 303 | 304 | unset($pages[$ts2]); 305 | } 306 | 307 | usleep(250000); 308 | } 309 | } 310 | ?> -------------------------------------------------------------------------------- /support/deflate_stream.php: -------------------------------------------------------------------------------- 1 | open = false; 13 | } 14 | 15 | public function __destruct() 16 | { 17 | $this->Finalize(); 18 | } 19 | 20 | public static function IsSupported() 21 | { 22 | if (!is_bool(self::$supported)) 23 | { 24 | self::$supported = function_exists("stream_filter_append") && function_exists("stream_filter_remove") && function_exists("gzcompress"); 25 | if (self::$supported) 26 | { 27 | $data = self::Compress("test"); 28 | if ($data === false || $data === "") self::$supported = false; 29 | else 30 | { 31 | $data = self::Uncompress($data); 32 | if ($data === false || $data !== "test") self::$supported = false; 33 | } 34 | } 35 | } 36 | 37 | return self::$supported; 38 | } 39 | 40 | public static function Compress($data, $compresslevel = -1, $options = array()) 41 | { 42 | $ds = new DeflateStream; 43 | if (!$ds->Init("wb", $compresslevel, $options)) return false; 44 | if (!$ds->Write($data)) return false; 45 | if (!$ds->Finalize()) return false; 46 | $data = $ds->Read(); 47 | 48 | return $data; 49 | } 50 | 51 | public static function Uncompress($data, $options = array("type" => "auto")) 52 | { 53 | $ds = new DeflateStream; 54 | if (!$ds->Init("rb", -1, $options)) return false; 55 | if (!$ds->Write($data)) return false; 56 | if (!$ds->Finalize()) return false; 57 | $data = $ds->Read(); 58 | 59 | return $data; 60 | } 61 | 62 | public function Init($mode, $compresslevel = -1, $options = array()) 63 | { 64 | if ($mode !== "rb" && $mode !== "wb") return false; 65 | if ($this->open) $this->Finalize(); 66 | 67 | $this->fp = fopen("php://memory", "w+b"); 68 | if ($this->fp === false) return false; 69 | $this->compress = ($mode == "wb"); 70 | if (!isset($options["type"])) $options["type"] = "rfc1951"; 71 | 72 | if ($options["type"] == "rfc1950") $options["type"] = "zlib"; 73 | else if ($options["type"] == "rfc1952") $options["type"] = "gzip"; 74 | 75 | if ($options["type"] != "zlib" && $options["type"] != "gzip" && ($this->compress || $options["type"] != "auto")) $options["type"] = "raw"; 76 | $this->options = $options; 77 | 78 | // Add the deflate filter. 79 | if ($this->compress) $this->filter = stream_filter_append($this->fp, "zlib.deflate", STREAM_FILTER_WRITE, $compresslevel); 80 | else $this->filter = stream_filter_append($this->fp, "zlib.inflate", STREAM_FILTER_READ); 81 | 82 | $this->open = true; 83 | $this->indata = ""; 84 | $this->outdata = ""; 85 | 86 | if ($this->compress) 87 | { 88 | if ($this->options["type"] == "zlib") 89 | { 90 | $this->outdata .= "\x78\x9C"; 91 | $this->options["a"] = 1; 92 | $this->options["b"] = 0; 93 | } 94 | else if ($this->options["type"] == "gzip") 95 | { 96 | if (!class_exists("CRC32Stream", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/crc32_stream.php"; 97 | 98 | $this->options["crc32"] = new CRC32Stream(); 99 | $this->options["crc32"]->Init(); 100 | $this->options["bytes"] = 0; 101 | 102 | $this->outdata .= "\x1F\x8B\x08"; 103 | $flags = 0; 104 | if (isset($this->options["filename"])) $flags |= 0x08; 105 | if (isset($this->options["comment"])) $flags |= 0x10; 106 | $this->outdata .= chr($flags); 107 | $this->outdata .= "\x00\x00\x00\x00"; 108 | $this->outdata .= "\x00"; 109 | $this->outdata .= "\x03"; 110 | 111 | if (isset($this->options["filename"])) $this->outdata .= str_replace("\x00", " ", $this->options["filename"]) . "\x00"; 112 | if (isset($this->options["comment"])) $this->outdata .= str_replace("\x00", " ", $this->options["comment"]) . "\x00"; 113 | } 114 | } 115 | else 116 | { 117 | $this->options["header"] = false; 118 | } 119 | 120 | return true; 121 | } 122 | 123 | public function Read() 124 | { 125 | $result = $this->outdata; 126 | $this->outdata = ""; 127 | 128 | return $result; 129 | } 130 | 131 | public function Write($data) 132 | { 133 | if (!$this->open) return false; 134 | 135 | if ($this->compress) 136 | { 137 | if ($this->options["type"] == "zlib") 138 | { 139 | // Adler-32. 140 | $y = strlen($data); 141 | for ($x = 0; $x < $y; $x++) 142 | { 143 | $this->options["a"] = ($this->options["a"] + ord($data[$x])) % 65521; 144 | $this->options["b"] = ($this->options["b"] + $this->options["a"]) % 65521; 145 | } 146 | } 147 | else if ($this->options["type"] == "gzip") 148 | { 149 | $this->options["crc32"]->AddData($data); 150 | $this->options["bytes"] = $this->ADD32($this->options["bytes"], strlen($data)); 151 | } 152 | 153 | $this->indata .= $data; 154 | while (strlen($this->indata) >= 65536) 155 | { 156 | fwrite($this->fp, substr($this->indata, 0, 65536)); 157 | $this->indata = substr($this->indata, 65536); 158 | 159 | $this->ProcessOutput(); 160 | } 161 | } 162 | else 163 | { 164 | $this->indata .= $data; 165 | $this->ProcessInput(); 166 | } 167 | 168 | return true; 169 | } 170 | 171 | // Finalizes the stream. 172 | public function Finalize() 173 | { 174 | if (!$this->open) return false; 175 | 176 | if (!$this->compress) $this->ProcessInput(true); 177 | 178 | if (strlen($this->indata) > 0) 179 | { 180 | fwrite($this->fp, $this->indata); 181 | $this->indata = ""; 182 | } 183 | 184 | // Removing the filter pushes the last buffer into the stream. 185 | stream_filter_remove($this->filter); 186 | $this->filter = false; 187 | 188 | $this->ProcessOutput(); 189 | 190 | fclose($this->fp); 191 | 192 | if ($this->compress) 193 | { 194 | if ($this->options["type"] == "zlib") $this->outdata .= pack("N", $this->SHL32($this->options["b"], 16) | $this->options["a"]); 195 | else if ($this->options["type"] == "gzip") $this->outdata .= pack("V", $this->options["crc32"]->Finalize()) . pack("V", $this->options["bytes"]); 196 | } 197 | 198 | $this->open = false; 199 | 200 | return true; 201 | } 202 | 203 | private function ProcessOutput() 204 | { 205 | rewind($this->fp); 206 | 207 | // Hack! Because ftell() on a stream with a filter is still broken even under the latest PHP a mere 11 years later. 208 | // See: https://bugs.php.net/bug.php?id=49874 209 | ob_start(); 210 | fpassthru($this->fp); 211 | $this->outdata .= ob_get_contents(); 212 | ob_end_clean(); 213 | 214 | rewind($this->fp); 215 | ftruncate($this->fp, 0); 216 | } 217 | 218 | private function ProcessInput($final = false) 219 | { 220 | // Automatically determine the type of data based on the header signature. 221 | if ($this->options["type"] == "auto") 222 | { 223 | if (strlen($this->indata) >= 3) 224 | { 225 | $zlibtest = unpack("n", substr($this->indata, 0, 2)); 226 | 227 | if (substr($this->indata, 0, 3) === "\x1F\x8B\x08") $this->options["type"] = "gzip"; 228 | else if ((ord($this->indata[0]) & 0x0F) == 8 && ((ord($this->indata[0]) & 0xF0) >> 4) < 8 && $zlibtest[1] % 31 == 0) $this->options["type"] = "zlib"; 229 | else $this->options["type"] = "raw"; 230 | } 231 | else if ($final) $this->options["type"] = "raw"; 232 | } 233 | 234 | if ($this->options["type"] == "gzip") 235 | { 236 | if (!$this->options["header"]) 237 | { 238 | if (strlen($this->indata) >= 10) 239 | { 240 | $idcm = substr($this->indata, 0, 3); 241 | $flg = ord($this->indata[3]); 242 | 243 | if ($idcm !== "\x1F\x8B\x08") $this->options["type"] = "ignore"; 244 | else 245 | { 246 | // Calculate the number of bytes to skip. If flags are set, the size can be dynamic. 247 | $size = 10; 248 | $y = strlen($this->indata); 249 | 250 | // FLG.FEXTRA 251 | if ($size && ($flg & 0x04)) 252 | { 253 | if ($size + 2 >= $y) $size = 0; 254 | else 255 | { 256 | $xlen = unpack("v", substr($this->indata, $size, 2)); 257 | $size = ($size + 2 + $xlen <= $y ? $size + 2 + $xlen : 0); 258 | } 259 | } 260 | 261 | // FLG.FNAME 262 | if ($size && ($flg & 0x08)) 263 | { 264 | $pos = strpos($this->indata, "\x00", $size); 265 | $size = ($pos !== false ? $pos + 1 : 0); 266 | } 267 | 268 | // FLG.FCOMMENT 269 | if ($size && ($flg & 0x10)) 270 | { 271 | $pos = strpos($this->indata, "\x00", $size); 272 | $size = ($pos !== false ? $pos + 1 : 0); 273 | } 274 | 275 | // FLG.FHCRC 276 | if ($size && ($flg & 0x02)) $size = ($size + 2 <= $y ? $size + 2 : 0); 277 | 278 | if ($size) 279 | { 280 | $this->indata = substr($this->indata, $size); 281 | $this->options["header"] = true; 282 | } 283 | } 284 | } 285 | } 286 | 287 | if ($this->options["header"] && strlen($this->indata) > 8) 288 | { 289 | fwrite($this->fp, substr($this->indata, 0, -8)); 290 | $this->indata = substr($this->indata, -8); 291 | 292 | $this->ProcessOutput(); 293 | } 294 | 295 | if ($final) $this->indata = ""; 296 | } 297 | else if ($this->options["type"] == "zlib") 298 | { 299 | if (!$this->options["header"]) 300 | { 301 | if (strlen($this->indata) >= 2) 302 | { 303 | $cmf = ord($this->indata[0]); 304 | $flg = ord($this->indata[1]); 305 | $cm = $cmf & 0x0F; 306 | $cinfo = ($cmf & 0xF0) >> 4; 307 | 308 | // Compression method 'deflate' ($cm = 8), window size - 8 ($cinfo < 8), no preset dictionaries ($flg bit 5), checksum validates. 309 | if ($cm != 8 || $cinfo > 7 || ($flg & 0x20) || (($cmf << 8 | $flg) % 31) != 0) $this->options["type"] = "ignore"; 310 | else 311 | { 312 | $this->indata = substr($this->indata, 2); 313 | $this->options["header"] = true; 314 | } 315 | } 316 | } 317 | 318 | if ($this->options["header"] && strlen($this->indata) > 4) 319 | { 320 | fwrite($this->fp, substr($this->indata, 0, -4)); 321 | $this->indata = substr($this->indata, -4); 322 | 323 | $this->ProcessOutput(); 324 | } 325 | 326 | if ($final) $this->indata = ""; 327 | } 328 | 329 | if ($this->options["type"] == "raw") 330 | { 331 | fwrite($this->fp, $this->indata); 332 | $this->indata = ""; 333 | 334 | $this->ProcessOutput(); 335 | } 336 | 337 | // Only set when an unrecoverable header error has occurred for gzip or zlib. 338 | if ($this->options["type"] == "ignore") $this->indata = ""; 339 | } 340 | 341 | private function SHL32($num, $bits) 342 | { 343 | if ($bits < 0) $bits = 0; 344 | 345 | return $this->LIM32((int)$num << $bits); 346 | } 347 | 348 | private function LIM32($num) 349 | { 350 | return (int)((int)$num & 0xFFFFFFFF); 351 | } 352 | 353 | private function ADD32($num, $num2) 354 | { 355 | $num = (int)$num; 356 | $num2 = (int)$num2; 357 | $add = ((($num >> 30) & 0x03) + (($num2 >> 30) & 0x03)); 358 | $num = ((int)($num & 0x3FFFFFFF) + (int)($num2 & 0x3FFFFFFF)); 359 | if ($num & 0x40000000) $add++; 360 | $num = (int)(($num & 0x3FFFFFFF) | (($add & 0x03) << 30)); 361 | 362 | return $num; 363 | } 364 | } 365 | ?> -------------------------------------------------------------------------------- /support/cli.php: -------------------------------------------------------------------------------- 1 | $val) 15 | { 16 | if (!isset($options["rules"][$val])) unset($options["shortmap"][$key]); 17 | } 18 | foreach ($options["rules"] as $key => $val) 19 | { 20 | if (!isset($val["arg"])) $options["rules"][$key]["arg"] = false; 21 | if (!isset($val["multiple"])) $options["rules"][$key]["multiple"] = false; 22 | } 23 | 24 | if ($args === false) $args = $_SERVER["argv"]; 25 | else if (is_string($args)) 26 | { 27 | $args2 = $args; 28 | $args = array(); 29 | $inside = false; 30 | $currarg = ""; 31 | $y = strlen($args2); 32 | for ($x = 0; $x < $y; $x++) 33 | { 34 | $currchr = substr($args2, $x, 1); 35 | 36 | if ($inside === false && $currchr == " " && $currarg != "") 37 | { 38 | $args[] = $currarg; 39 | $currarg = ""; 40 | } 41 | else if ($currchr == "\"" || $currchr == "'") 42 | { 43 | if ($inside === false) $inside = $currchr; 44 | else if ($inside === $currchr) $inside = false; 45 | else $currarg .= $currchr; 46 | } 47 | else if ($currchr == "\\" && $x < $y - 1) 48 | { 49 | $x++; 50 | $currarg .= substr($args2, $x, 1); 51 | } 52 | else if ($inside !== false || $currchr != " ") 53 | { 54 | $currarg .= $currchr; 55 | } 56 | } 57 | 58 | if ($currarg != "") $args[] = $currarg; 59 | } 60 | 61 | $result = array("success" => true, "file" => array_shift($args), "opts" => array(), "params" => array()); 62 | 63 | // Look over shortmap to determine if options exist that are one byte (flags) and don't have arguments. 64 | $chrs = array(); 65 | foreach ($options["shortmap"] as $key => $val) 66 | { 67 | if (isset($options["rules"][$val]) && !$options["rules"][$val]["arg"]) $chrs[$key] = true; 68 | } 69 | 70 | $allowopt = true; 71 | $y = count($args); 72 | for ($x = 0; $x < $y; $x++) 73 | { 74 | $arg = $args[$x]; 75 | 76 | // Attempt to process an option. 77 | $opt = false; 78 | $optval = false; 79 | if ($allowopt && substr($arg, 0, 1) == "-") 80 | { 81 | $pos = strpos($arg, "="); 82 | if ($pos === false) $pos = strlen($arg); 83 | else $optval = substr($arg, $pos + 1); 84 | $arg2 = substr($arg, 1, $pos - 1); 85 | 86 | if (isset($options["rules"][$arg2])) $opt = $arg2; 87 | else if (isset($options["shortmap"][$arg2])) $opt = $options["shortmap"][$arg2]; 88 | else if ($x == 0) 89 | { 90 | // Attempt to process as a set of flags. 91 | $y2 = strlen($arg2); 92 | if ($y2 > 0) 93 | { 94 | for ($x2 = 0; $x2 < $y2; $x2++) 95 | { 96 | $currchr = substr($arg2, $x2, 1); 97 | 98 | if (!isset($chrs[$currchr])) break; 99 | } 100 | 101 | if ($x2 == $y2) 102 | { 103 | for ($x2 = 0; $x2 < $y2; $x2++) 104 | { 105 | $opt = $options["shortmap"][substr($arg2, $x2, 1)]; 106 | 107 | if (!$options["rules"][$opt]["multiple"]) $result["opts"][$opt] = true; 108 | else 109 | { 110 | if (!isset($result["opts"][$opt])) $result["opts"][$opt] = 0; 111 | $result["opts"][$opt]++; 112 | } 113 | } 114 | 115 | continue; 116 | } 117 | } 118 | } 119 | } 120 | 121 | if ($opt === false) 122 | { 123 | // Is a parameter. 124 | if (substr($arg, 0, 1) === "\"" || substr($arg, 0, 1) === "'") $arg = substr($arg, 1); 125 | if (substr($arg, -1) === "\"" || substr($arg, -1) === "'") $arg = substr($arg, 0, -1); 126 | 127 | $result["params"][] = $arg; 128 | 129 | if (!$options["allow_opts_after_param"]) $allowopt = false; 130 | } 131 | else if (!$options["rules"][$opt]["arg"]) 132 | { 133 | // Is a flag by itself. 134 | if (!$options["rules"][$opt]["multiple"]) $result["opts"][$opt] = true; 135 | else 136 | { 137 | if (!isset($result["opts"][$opt])) $result["opts"][$opt] = 0; 138 | $result["opts"][$opt]++; 139 | } 140 | } 141 | else 142 | { 143 | // Is an option. 144 | if ($optval === false) 145 | { 146 | $x++; 147 | if ($x == $y) break; 148 | $optval = $args[$x]; 149 | } 150 | 151 | if (substr($optval, 0, 1) === "\"" || substr($optval, 0, 1) === "'") $optval = substr($optval, 1); 152 | if (substr($optval, -1) === "\"" || substr($optval, -1) === "'") $optval = substr($optval, 0, -1); 153 | 154 | if (!$options["rules"][$opt]["multiple"]) $result["opts"][$opt] = $optval; 155 | else 156 | { 157 | if (!isset($result["opts"][$opt])) $result["opts"][$opt] = array(); 158 | $result["opts"][$opt][] = $optval; 159 | } 160 | } 161 | } 162 | 163 | return $result; 164 | } 165 | 166 | public static function CanGetUserInputWithArgs(&$args, $prefix) 167 | { 168 | return (($prefix !== false && isset($args["opts"][$prefix]) && is_array($args["opts"][$prefix]) && count($args["opts"][$prefix])) || count($args["params"])); 169 | } 170 | 171 | // Gets a line of input from the user. If the user supplies all information via the command-line, this could be entirely automated. 172 | public static function GetUserInputWithArgs(&$args, $prefix, $question, $default, $noparamsoutput = "", $suppressoutput = false, $callback = false, $callbackopts = false) 173 | { 174 | if (!self::CanGetUserInputWithArgs($args, $prefix) && $noparamsoutput != "") 175 | { 176 | echo "\n" . rtrim($noparamsoutput) . "\n\n"; 177 | 178 | $suppressoutput = false; 179 | $noparamsoutput = ""; 180 | } 181 | 182 | do 183 | { 184 | $prompt = ($suppressoutput ? "" : $question . ($default !== false ? " [" . $default . "]" : "") . ": "); 185 | 186 | if ($prefix !== false && isset($args["opts"][$prefix]) && is_array($args["opts"][$prefix]) && count($args["opts"][$prefix])) 187 | { 188 | $line = array_shift($args["opts"][$prefix]); 189 | if ($line === "") $line = $default; 190 | if (!$suppressoutput) echo $prompt . $line . "\n"; 191 | } 192 | else if (count($args["params"])) 193 | { 194 | $line = array_shift($args["params"]); 195 | if ($line === "") $line = $default; 196 | if (!$suppressoutput) echo $prompt . $line . "\n"; 197 | } 198 | else if (strtoupper(substr(php_uname("s"), 0, 3)) != "WIN" && function_exists("readline") && function_exists("readline_add_history")) 199 | { 200 | $line = readline($prompt); 201 | if ($line === false) exit(); 202 | 203 | $line = trim($line); 204 | if ($line === "") $line = $default; 205 | if ($line !== false && $line !== "") readline_add_history($line); 206 | } 207 | else 208 | { 209 | echo $prompt; 210 | fflush(STDOUT); 211 | $line = fgets(STDIN); 212 | if ($line === false || ($line === "" && feof(STDIN))) exit(); 213 | 214 | $line = trim($line); 215 | if ($line === "") $line = $default; 216 | } 217 | 218 | if ($line === false || (is_callable($callback) && !call_user_func_array($callback, array($line, &$callbackopts)))) 219 | { 220 | if ($line !== false) $line = false; 221 | else echo "Please enter a value.\n"; 222 | 223 | if (!self::CanGetUserInputWithArgs($args, $prefix) && $noparamsoutput != "") 224 | { 225 | echo "\n" . $noparamsoutput . "\n"; 226 | 227 | $noparamsoutput = ""; 228 | } 229 | 230 | $suppressoutput = false; 231 | } 232 | } while ($line === false); 233 | 234 | return $line; 235 | } 236 | 237 | // Obtains a valid line of input. 238 | public static function GetLimitedUserInputWithArgs(&$args, $prefix, $question, $default, $allowedoptionsprefix, $allowedoptions, $loop = true, $suppressoutput = false, $multipleuntil = false) 239 | { 240 | $noparamsoutput = $allowedoptionsprefix . "\n\n"; 241 | $size = 0; 242 | foreach ($allowedoptions as $key => $val) 243 | { 244 | if ($size < strlen($key)) $size = strlen($key); 245 | } 246 | 247 | foreach ($allowedoptions as $key => $val) 248 | { 249 | $newtab = str_repeat(" ", 2 + $size + 3); 250 | $noparamsoutput .= " " . $key . ":" . str_repeat(" ", $size - strlen($key)) . " " . str_replace("\n\t", "\n" . $newtab, $val) . "\n"; 251 | } 252 | 253 | $noparamsoutput .= "\n"; 254 | 255 | if ($default === false && count($allowedoptions) == 1) 256 | { 257 | reset($allowedoptions); 258 | $default = key($allowedoptions); 259 | } 260 | 261 | $results = array(); 262 | do 263 | { 264 | $displayed = (!count($args["params"])); 265 | $result = self::GetUserInputWithArgs($args, $prefix, $question, $default, $noparamsoutput, $suppressoutput); 266 | if (is_array($multipleuntil) && $multipleuntil["exit"] === $result) break; 267 | $result2 = false; 268 | if (!count($allowedoptions)) break; 269 | foreach ($allowedoptions as $key => $val) 270 | { 271 | if (!strcasecmp($key, $result) || !strcasecmp($val, $result)) $result2 = $key; 272 | } 273 | if ($loop) 274 | { 275 | if ($result2 === false) 276 | { 277 | echo "Please select an option from the list.\n"; 278 | 279 | $suppressoutput = false; 280 | } 281 | else if (is_array($multipleuntil)) 282 | { 283 | $results[$result2] = $result2; 284 | 285 | $question = $multipleuntil["nextquestion"]; 286 | $default = $multipleuntil["nextdefault"]; 287 | } 288 | } 289 | 290 | if ($displayed) $noparamsoutput = ""; 291 | } while ($loop && ($result2 === false || is_array($multipleuntil))); 292 | 293 | return (is_array($multipleuntil) ? $results : $result2); 294 | } 295 | 296 | // Obtains Yes/No style input. 297 | public static function GetYesNoUserInputWithArgs(&$args, $prefix, $question, $default, $noparamsoutput = "", $suppressoutput = false) 298 | { 299 | $default = (substr(strtoupper(trim($default)), 0, 1) === "Y" ? "Y" : "N"); 300 | 301 | $result = self::GetUserInputWithArgs($args, $prefix, $question, $default, $noparamsoutput, $suppressoutput); 302 | $result = (substr(strtoupper(trim($result)), 0, 1) === "Y"); 303 | 304 | return $result; 305 | } 306 | 307 | public static function GetHexDump($data) 308 | { 309 | $result = ""; 310 | 311 | $x = 0; 312 | $y = strlen($data); 313 | if ($y <= 256) $padwidth = 2; 314 | else if ($y <= 65536) $padwidth = 4; 315 | else if ($y <= 16777216) $padwidth = 6; 316 | else $padwidth = 8; 317 | 318 | $pad = str_repeat(" ", $padwidth); 319 | 320 | $data2 = str_split(strtoupper(bin2hex($data)), 32); 321 | foreach ($data2 as $line) 322 | { 323 | $result .= sprintf("%0" . $padwidth . "X", $x) . " | "; 324 | 325 | $line = str_split($line, 2); 326 | array_splice($line, 8, 0, ""); 327 | $result .= implode(" ", $line) . "\n"; 328 | 329 | $result .= $pad . " |"; 330 | $y2 = $x + 16; 331 | for ($x2 = 0; $x2 < 16 && $x < $y; $x2++) 332 | { 333 | $result .= " "; 334 | if ($x2 === 8) $result .= " "; 335 | 336 | $tempchr = ord($data[$x]); 337 | if ($tempchr === 0x09) $result .= "\\t"; 338 | else if ($tempchr === 0x0D) $result .= "\\r"; 339 | else if ($tempchr === 0x0A) $result .= "\\n"; 340 | else if ($tempchr === 0x00) $result .= "\\0"; 341 | else if ($tempchr < 32 || $tempchr > 126) $result .= " "; 342 | else $result .= " " . $data[$x]; 343 | 344 | $x++; 345 | } 346 | 347 | $result .= "\n"; 348 | } 349 | 350 | return $result; 351 | } 352 | 353 | // Outputs a JSON array (useful for captured output). 354 | public static function DisplayResult($result, $exit = true) 355 | { 356 | if (is_array($result)) echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; 357 | else echo $result . "\n"; 358 | 359 | if ($exit) exit(); 360 | } 361 | 362 | // Useful for reparsing remaining parameters as new arguments. 363 | public static function ReinitArgs(&$args, $newargs) 364 | { 365 | // Process the parameters. 366 | $options = array( 367 | "shortmap" => array( 368 | "?" => "help" 369 | ), 370 | "rules" => array( 371 | ) 372 | ); 373 | 374 | foreach ($newargs as $arg) $options["rules"][$arg] = array("arg" => true, "multiple" => true); 375 | $options["rules"]["help"] = array("arg" => false); 376 | 377 | $args = self::ParseCommandLine($options, array_merge(array(""), $args["params"])); 378 | 379 | if (isset($args["opts"]["help"])) self::DisplayResult(array("success" => true, "options" => array_keys($options["rules"]))); 380 | } 381 | 382 | // Tracks messages for a command-line interface app. 383 | private static $messages = array(); 384 | 385 | public static function LogMessage($msg, $data = null) 386 | { 387 | if (isset($data)) $msg .= "\n\t" . trim(str_replace("\n", "\n\t", json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES))); 388 | 389 | self::$messages[] = $msg; 390 | 391 | fwrite(STDERR, $msg . "\n"); 392 | } 393 | 394 | public static function DisplayError($msg, $result = false, $exit = true) 395 | { 396 | self::LogMessage(($exit ? "[Error] " : "") . $msg); 397 | 398 | if ($result !== false && is_array($result) && isset($result["error"]) && isset($result["errorcode"])) self::LogMessage("[Error] " . $result["error"] . " (" . $result["errorcode"] . ")", (isset($result["info"]) ? $result["info"] : null)); 399 | 400 | if ($exit) exit(); 401 | } 402 | 403 | public static function GetLogMessages($filters = array()) 404 | { 405 | if (is_string($filters)) $filters = array($filters); 406 | 407 | $result = array(); 408 | foreach (self::$messages as $message) 409 | { 410 | $found = (!count($filters)); 411 | foreach ($filters as $filter) 412 | { 413 | if (preg_match($filter, $message)) $found = true; 414 | } 415 | 416 | if ($found) $result[] = $message; 417 | } 418 | 419 | return $result; 420 | } 421 | 422 | public static function ResetLogMessages() 423 | { 424 | self::$messages = array(); 425 | } 426 | 427 | 428 | private static $timerinfo = array(); 429 | 430 | public static function StartTimer() 431 | { 432 | $ts = microtime(true); 433 | 434 | self::$timerinfo = array( 435 | "start" => $ts, 436 | "diff" => $ts 437 | ); 438 | } 439 | 440 | public static function UpdateTimer() 441 | { 442 | $ts = microtime(true); 443 | $diff = $ts - self::$timerinfo["diff"]; 444 | self::$timerinfo["diff"] = $ts; 445 | 446 | $result = array( 447 | "success" => true, 448 | "diff" => sprintf("%.2f", $diff), 449 | "total" => sprintf("%.2f", $ts - self::$timerinfo["start"]) 450 | ); 451 | 452 | return $result; 453 | } 454 | } 455 | ?> -------------------------------------------------------------------------------- /support/utf_utils.php: -------------------------------------------------------------------------------- 1 | = 0x0300 && $val <= 0x036F) || ($val >= 0x1DC0 && $val <= 0x1DFF) || ($val >= 0x20D0 && $val <= 0x20FF) || ($val >= 0xFE20 && $val <= 0xFE2F)); 21 | } 22 | 23 | public static function Convert($data, $srctype, $desttype) 24 | { 25 | $arr = is_array($data); 26 | if ($arr) $srctype = self::UTF32_ARRAY; 27 | $x = 0; 28 | $y = ($arr ? count($data) : strlen($data)); 29 | $result = ($desttype === self::UTF32_ARRAY ? array() : ""); 30 | if (!$arr && $srctype === self::UTF32_ARRAY) return $result; 31 | 32 | $first = true; 33 | 34 | if ($srctype === self::UTF8_BOM) 35 | { 36 | if (substr($data, 0, 3) === "\xEF\xBB\xBF") $x = 3; 37 | 38 | $srctype = self::UTF8; 39 | } 40 | 41 | if ($srctype === self::UTF16_BOM) 42 | { 43 | if (substr($data, 0, 2) === "\xFE\xFF") 44 | { 45 | $srctype = self::UTF16_BE; 46 | $x = 2; 47 | } 48 | else if (substr($data, 0, 2) === "\xFF\xFE") 49 | { 50 | $srctype = self::UTF16_LE; 51 | $x = 2; 52 | } 53 | else 54 | { 55 | $srctype = self::UTF16_LE; 56 | } 57 | } 58 | 59 | if ($srctype === self::UTF32_BOM) 60 | { 61 | if (substr($data, 0, 4) === "\x00\x00\xFE\xFF") 62 | { 63 | $srctype = self::UTF32_BE; 64 | $x = 4; 65 | } 66 | else if (substr($data, 0, 4) === "\xFF\xFE\x00\x00") 67 | { 68 | $srctype = self::UTF32_LE; 69 | $x = 4; 70 | } 71 | else 72 | { 73 | $srctype = self::UTF32_LE; 74 | } 75 | } 76 | 77 | while ($x < $y) 78 | { 79 | // Read the next valid code point. 80 | $val = false; 81 | 82 | switch ($srctype) 83 | { 84 | case self::UTF8: 85 | { 86 | $tempchr = ord($data[$x]); 87 | if ($tempchr <= 0x7F) 88 | { 89 | $val = $tempchr; 90 | $x++; 91 | } 92 | else if ($tempchr < 0xC2) $x++; 93 | else 94 | { 95 | $left = $y - $x; 96 | if ($left < 2) $x++; 97 | else 98 | { 99 | $tempchr2 = ord($data[$x + 1]); 100 | 101 | if (($tempchr >= 0xC2 && $tempchr <= 0xDF) && ($tempchr2 >= 0x80 && $tempchr2 <= 0xBF)) 102 | { 103 | $val = (($tempchr & 0x1F) << 6) | ($tempchr2 & 0x3F); 104 | $x += 2; 105 | } 106 | else if ($left < 3) $x++; 107 | else 108 | { 109 | $tempchr3 = ord($data[$x + 2]); 110 | 111 | if ($tempchr3 < 0x80 || $tempchr3 > 0xBF) $x++; 112 | else 113 | { 114 | if (($tempchr == 0xE0 && ($tempchr2 >= 0xA0 && $tempchr2 <= 0xBF)) || ((($tempchr >= 0xE1 && $tempchr <= 0xEC) || $tempchr == 0xEE || $tempchr == 0xEF) && ($tempchr2 >= 0x80 && $tempchr2 <= 0xBF)) || ($tempchr == 0xED && ($tempchr2 >= 0x80 && $tempchr2 <= 0x9F))) 115 | { 116 | $val = (($tempchr & 0x0F) << 12) | (($tempchr2 & 0x3F) << 6) | ($tempchr3 & 0x3F); 117 | $x += 3; 118 | } 119 | else if ($left < 4) $x++; 120 | else 121 | { 122 | $tempchr4 = ord($data[$x + 3]); 123 | 124 | if ($tempchr4 < 0x80 || $tempchr4 > 0xBF) $x++; 125 | else if (($tempchr == 0xF0 && ($tempchr2 >= 0x90 && $tempchr2 <= 0xBF)) || (($tempchr >= 0xF1 && $tempchr <= 0xF3) && ($tempchr2 >= 0x80 && $tempchr2 <= 0xBF)) || ($tempchr == 0xF4 && ($tempchr2 >= 0x80 && $tempchr2 <= 0x8F))) 126 | { 127 | $val = (($tempchr & 0x07) << 18) | (($tempchr2 & 0x3F) << 12) | (($tempchr3 & 0x3F) << 6) | ($tempchr4 & 0x3F); 128 | $x += 4; 129 | } 130 | else 131 | { 132 | $x++; 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | break; 141 | } 142 | case self::UTF16_LE: 143 | { 144 | if ($x + 1 >= $y) $x = $y; 145 | else 146 | { 147 | $val = unpack("v", substr($data, $x, 2))[1]; 148 | $x += 2; 149 | 150 | if ($val >= 0xD800 && $val <= 0xDBFF) 151 | { 152 | if ($x + 1 >= $y) 153 | { 154 | $x = $y; 155 | $val = false; 156 | } 157 | else 158 | { 159 | $val2 = unpack("v", substr($data, $x, 2))[1]; 160 | 161 | if ($val2 < 0xDC00 || $val2 > 0xDFFF) $val = false; 162 | else 163 | { 164 | $val = ((($val - 0xD800) << 10) | ($val2 - 0xDC00)) + 0x10000; 165 | $x += 2; 166 | } 167 | } 168 | } 169 | } 170 | 171 | break; 172 | } 173 | case self::UTF16_BE: 174 | { 175 | if ($x + 1 >= $y) $x = $y; 176 | else 177 | { 178 | $val = unpack("n", substr($data, $x, 2))[1]; 179 | $x += 2; 180 | 181 | if ($val >= 0xD800 && $val <= 0xDBFF) 182 | { 183 | if ($x + 1 >= $y) 184 | { 185 | $x = $y; 186 | $val = false; 187 | } 188 | else 189 | { 190 | $val2 = unpack("n", substr($data, $x, 2))[1]; 191 | 192 | if ($val2 < 0xDC00 || $val2 > 0xDFFF) $val = false; 193 | else 194 | { 195 | $val = ((($val - 0xD800) << 10) | ($val2 - 0xDC00)) + 0x10000; 196 | $x += 2; 197 | } 198 | } 199 | } 200 | } 201 | 202 | break; 203 | } 204 | case self::UTF32_LE: 205 | { 206 | if ($x + 3 >= $y) $x = $y; 207 | else 208 | { 209 | $val = unpack("V", substr($data, $x, 4))[1]; 210 | $x += 4; 211 | } 212 | 213 | break; 214 | } 215 | case self::UTF32_BE: 216 | { 217 | if ($x + 3 >= $y) $x = $y; 218 | else 219 | { 220 | $val = unpack("N", substr($data, $x, 4))[1]; 221 | $x += 4; 222 | } 223 | 224 | break; 225 | } 226 | case self::UTF32_ARRAY: 227 | { 228 | $val = (int)$data[$x]; 229 | $x++; 230 | 231 | break; 232 | } 233 | default: $x = $y; break; 234 | } 235 | 236 | // Make sure it is a valid Unicode value. 237 | // 0xD800-0xDFFF are for UTF-16 surrogate pairs. Invalid characters. 238 | // 0xFDD0-0xFDEF are non-characters. 239 | // 0x*FFFE and 0x*FFFF are reserved. 240 | // The largest possible character is 0x10FFFF. 241 | // First character can't be a combining code point. 242 | if ($val !== false && !($val < 0 || ($val >= 0xD800 && $val <= 0xDFFF) || ($val >= 0xFDD0 && $val <= 0xFDEF) || ($val & 0xFFFE) == 0xFFFE || $val > 0x10FFFF || ($first && self::IsCombiningCodePoint($val)))) 243 | { 244 | if ($first) 245 | { 246 | if ($desttype === self::UTF8_BOM) 247 | { 248 | $result .= "\xEF\xBB\xBF"; 249 | 250 | $desttype = self::UTF8; 251 | } 252 | 253 | if ($desttype === self::UTF16_BOM) 254 | { 255 | $result .= "\xFF\xFE"; 256 | 257 | $desttype = self::UTF16_LE; 258 | } 259 | 260 | if ($srctype === self::UTF32_BOM) 261 | { 262 | $result .= "\xFF\xFE\x00\x00"; 263 | 264 | $desttype = self::UTF32_LE; 265 | } 266 | 267 | $first = false; 268 | } 269 | 270 | switch ($desttype) 271 | { 272 | case self::UTF8: 273 | { 274 | if ($val <= 0x7F) $result .= chr($val); 275 | else if ($val <= 0x7FF) $result .= chr(0xC0 | ($val >> 6)) . chr(0x80 | ($val & 0x3F)); 276 | else if ($val <= 0xFFFF) $result .= chr(0xE0 | ($val >> 12)) . chr(0x80 | (($val >> 6) & 0x3F)) . chr(0x80 | ($val & 0x3F)); 277 | else if ($val <= 0x10FFFF) $result .= chr(0xF0 | ($val >> 18)) . chr(0x80 | (($val >> 12) & 0x3F)) . chr(0x80 | (($val >> 6) & 0x3F)) . chr(0x80 | ($val & 0x3F)); 278 | 279 | break; 280 | } 281 | case self::UTF16_LE: 282 | { 283 | if ($val <= 0xFFFF) $result .= pack("v", $val); 284 | else 285 | { 286 | $val -= 0x10000; 287 | $result .= pack("v", ((($val >> 10) & 0x3FF) + 0xD800)); 288 | $result .= pack("v", (($val & 0x3FF) + 0xDC00)); 289 | } 290 | 291 | break; 292 | } 293 | case self::UTF16_BE: 294 | { 295 | if ($val <= 0xFFFF) $result .= pack("n", $val); 296 | else 297 | { 298 | $val -= 0x10000; 299 | $result .= pack("n", ((($val >> 10) & 0x3FF) + 0xD800)); 300 | $result .= pack("n", (($val & 0x3FF) + 0xDC00)); 301 | } 302 | 303 | break; 304 | } 305 | case self::UTF32_LE: 306 | { 307 | $result .= pack("V", $val); 308 | 309 | break; 310 | } 311 | case self::UTF32_BE: 312 | { 313 | $result .= pack("N", $val); 314 | 315 | break; 316 | } 317 | case self::UTF32_ARRAY: 318 | { 319 | $result[] = $val; 320 | 321 | break; 322 | } 323 | default: $x = $y; break; 324 | } 325 | } 326 | } 327 | 328 | return $result; 329 | } 330 | 331 | 332 | protected const PUNYCODE_BASE = 36; 333 | protected const PUNYCODE_TMIN = 1; 334 | protected const PUNYCODE_TMAX = 26; 335 | protected const PUNYCODE_SKEW = 38; 336 | protected const PUNYCODE_DAMP = 700; 337 | protected const PUNYCODE_INITIAL_BIAS = 72; 338 | protected const PUNYCODE_INITIAL_N = 0x80; 339 | protected const PUNYCODE_DIGIT_MAP = "abcdefghijklmnopqrstuvwxyz0123456789"; 340 | 341 | public static function ConvertToPunycode($domain) 342 | { 343 | // Reject invalid domain name lengths. 344 | if (strlen($domain) > 255) return false; 345 | 346 | $parts = explode(".", $domain); 347 | 348 | foreach ($parts as $num => $part) 349 | { 350 | // Reject invalid label lengths. 351 | $y = strlen($part); 352 | if ($y > 63) return false; 353 | 354 | // Skip already encoded portions. 355 | if (substr($part, 0, 4) === "xn--") continue; 356 | 357 | // Convert UTF-8 to UTF-32 code points. 358 | $data = self::Convert($part, self::UTF8, self::UTF32_ARRAY); 359 | 360 | // Handle ASCII code points. 361 | $part2 = ""; 362 | foreach ($data as $cp) 363 | { 364 | if ($cp <= 0x7F) $part2 .= strtolower(chr($cp)); 365 | } 366 | 367 | $numhandled = strlen($part2); 368 | $y = count($data); 369 | 370 | if ($numhandled >= $y) 371 | { 372 | $parts[$num] = $part2; 373 | 374 | continue; 375 | } 376 | 377 | if ($numhandled) $part2 .= "-"; 378 | 379 | $part2 = "xn--" . $part2; 380 | 381 | if (strlen($part2) > 63) return false; 382 | 383 | $bias = self::PUNYCODE_INITIAL_BIAS; 384 | $n = self::PUNYCODE_INITIAL_N; 385 | $delta = 0; 386 | $first = true; 387 | 388 | while ($numhandled < $y) 389 | { 390 | // Find the next largest unhandled code point. 391 | $cp2 = 0x01000000; 392 | foreach ($data as $cp) 393 | { 394 | if ($cp >= $n && $cp2 > $cp) $cp2 = $cp; 395 | } 396 | 397 | // Increase delta but prevent overflow. 398 | $delta += ($cp2 - $n) * ($numhandled + 1); 399 | if ($delta < 0) return false; 400 | $n = $cp2; 401 | 402 | foreach ($data as $cp) 403 | { 404 | if ($cp < $n) 405 | { 406 | $delta++; 407 | 408 | if ($delta < 0) return false; 409 | } 410 | else if ($cp === $n) 411 | { 412 | // Calculate and encode a variable length integer from the delta. 413 | $q = $delta; 414 | $x = 0; 415 | do 416 | { 417 | $x += self::PUNYCODE_BASE; 418 | 419 | if ($x <= $bias) $t = self::PUNYCODE_TMIN; 420 | else if ($x >= $bias + self::PUNYCODE_TMAX) $t = self::PUNYCODE_TMAX; 421 | else $t = $x - $bias; 422 | 423 | if ($q < $t) break; 424 | 425 | $part2 .= self::PUNYCODE_DIGIT_MAP[$t + (($q - $t) % (self::PUNYCODE_BASE - $t))]; 426 | 427 | $q = (int)(($q - $t) / (self::PUNYCODE_BASE - $t)); 428 | 429 | if (strlen($part2) > 63) return false; 430 | } while (1); 431 | 432 | $part2 .= self::PUNYCODE_DIGIT_MAP[$q]; 433 | if (strlen($part2) > 63) return false; 434 | 435 | // Adapt bias. 436 | $numhandled++; 437 | $bias = self::InternalPunycodeAdapt($delta, $numhandled, $first); 438 | $delta = 0; 439 | $first = false; 440 | } 441 | } 442 | 443 | $delta++; 444 | $n++; 445 | } 446 | 447 | $parts[$num] = $part2; 448 | } 449 | 450 | return implode(".", $parts); 451 | } 452 | 453 | public static function ConvertFromPunycode($domain) 454 | { 455 | // Reject invalid domain name lengths. 456 | if (strlen($domain) > 255) return false; 457 | 458 | $parts = explode(".", $domain); 459 | 460 | foreach ($parts as $num => $part) 461 | { 462 | // Reject invalid label lengths. 463 | $y = strlen($part); 464 | if ($y > 63) return false; 465 | 466 | // Skip unencoded portions. 467 | if (substr($part, 0, 4) !== "xn--") continue; 468 | 469 | $part = substr($part, 4); 470 | 471 | // Convert UTF-8 to UTF-32 code points. 472 | $data = self::Convert($part, self::UTF8, self::UTF32_ARRAY); 473 | 474 | // Handle ASCII code points. 475 | $hyphen = ord("-"); 476 | for ($x = count($data); $x && $data[$x - 1] !== $hyphen; $x--); 477 | if (!$x) $data2 = array(); 478 | else 479 | { 480 | $data2 = array_splice($data, 0, $x - 1); 481 | 482 | array_shift($data); 483 | } 484 | 485 | $numhandled = count($data2); 486 | 487 | $bias = self::PUNYCODE_INITIAL_BIAS; 488 | $n = self::PUNYCODE_INITIAL_N; 489 | $delta = 0; 490 | $first = true; 491 | 492 | $pos = 0; 493 | $y = count($data); 494 | while ($pos < $y) 495 | { 496 | // Calculate and decode a delta from the variable length integer. 497 | $olddelta = $delta; 498 | $w = 1; 499 | $x = 0; 500 | do 501 | { 502 | $x += self::PUNYCODE_BASE; 503 | 504 | $cp = $data[$pos]; 505 | $pos++; 506 | 507 | if ($cp >= ord("a") && $cp <= ord("z")) $digit = $cp - ord("a"); 508 | else if ($cp >= ord("A") && $cp <= ord("Z")) $digit = $cp - ord("A"); 509 | else if ($cp >= ord("0") && $cp <= ord("9")) $digit = $cp - ord("0") + 26; 510 | else return false; 511 | 512 | $delta += $digit * $w; 513 | if ($delta < 0) return false; 514 | 515 | if ($x <= $bias) $t = self::PUNYCODE_TMIN; 516 | else if ($x >= $bias + self::PUNYCODE_TMAX) $t = self::PUNYCODE_TMAX; 517 | else $t = $x - $bias; 518 | 519 | if ($digit < $t) break; 520 | 521 | $w *= (self::PUNYCODE_BASE - $t); 522 | if ($w < 0) return false; 523 | } while (1); 524 | 525 | // Adapt bias. 526 | $numhandled++; 527 | $bias = self::InternalPunycodeAdapt($delta - $olddelta, $numhandled, $first); 528 | $first = false; 529 | 530 | // Delta was supposed to wrap around from $numhandled to 0, incrementing $n each time, so fix that now. 531 | $n += (int)($delta / $numhandled); 532 | $delta %= $numhandled; 533 | 534 | // Insert $n (the code point) at the delta position. 535 | array_splice($data2, $delta, 0, array($n)); 536 | $delta++; 537 | } 538 | 539 | $parts[$num] = self::Convert($data2, self::UTF32_ARRAY, self::UTF8); 540 | } 541 | 542 | return implode(".", $parts); 543 | } 544 | 545 | // RFC3492 adapt() function. 546 | protected static function InternalPunycodeAdapt($delta, $numpoints, $first) 547 | { 548 | $delta = ($first ? (int)($delta / self::PUNYCODE_DAMP) : $delta >> 1); 549 | $delta += (int)($delta / $numpoints); 550 | 551 | $y = self::PUNYCODE_BASE - self::PUNYCODE_TMIN; 552 | 553 | $condval = (int)(($y * self::PUNYCODE_TMAX) / 2); 554 | for ($x = 0; $delta > $condval; $x += self::PUNYCODE_BASE) $delta = (int)($delta / $y); 555 | 556 | return (int)($x + ((($y + 1) * $delta) / ($delta + self::PUNYCODE_SKEW))); 557 | } 558 | } 559 | ?> -------------------------------------------------------------------------------- /support/generic_server.php: -------------------------------------------------------------------------------- 1 | Reset(); 17 | } 18 | 19 | public function Reset() 20 | { 21 | $this->debug = false; 22 | $this->fp = false; 23 | $this->ssl = false; 24 | $this->initclients = array(); 25 | $this->clients = array(); 26 | $this->nextclientid = 1; 27 | 28 | $this->defaulttimeout = 30; 29 | $this->defaultclienttimeout = 30; 30 | $this->lasttimeoutcheck = microtime(true); 31 | } 32 | 33 | public function __destruct() 34 | { 35 | $this->Stop(); 36 | } 37 | 38 | public function SetDebug($debug) 39 | { 40 | $this->debug = (bool)$debug; 41 | } 42 | 43 | public function SetDefaultTimeout($timeout) 44 | { 45 | $this->defaulttimeout = (int)$timeout; 46 | } 47 | 48 | public function SetDefaultClientTimeout($timeout) 49 | { 50 | $this->defaultclienttimeout = (int)$timeout; 51 | } 52 | 53 | public static function GetSSLCiphers($type = "intermediate") 54 | { 55 | $type = strtolower($type); 56 | 57 | // Cipher list last updated May 3, 2017. 58 | if ($type == "modern") return "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"; 59 | 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"; 60 | 61 | return "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS"; 62 | } 63 | 64 | // Starts the server on the host and port. 65 | // $host is usually 0.0.0.0 or 127.0.0.1 for IPv4 and [::0] or [::1] for IPv6. 66 | public function Start($host, $port, $sslopts = false) 67 | { 68 | $this->Stop(); 69 | 70 | $context = stream_context_create(); 71 | 72 | if (is_array($sslopts)) 73 | { 74 | stream_context_set_option($context, "ssl", "ciphers", self::GetSSLCiphers()); 75 | stream_context_set_option($context, "ssl", "disable_compression", true); 76 | stream_context_set_option($context, "ssl", "allow_self_signed", true); 77 | stream_context_set_option($context, "ssl", "verify_peer", false); 78 | 79 | // 'local_cert' and 'local_pk' are common options. 80 | foreach ($sslopts as $key => $val) 81 | { 82 | stream_context_set_option($context, "ssl", $key, $val); 83 | } 84 | 85 | $this->ssl = true; 86 | } 87 | 88 | $this->fp = stream_socket_server("tcp://" . $host . ":" . $port, $errornum, $errorstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context); 89 | if ($this->fp === false) return array("success" => false, "error" => self::GSTranslate("Bind() failed. Reason: %s (%d)", $errorstr, $errornum), "errorcode" => "bind_failed"); 90 | 91 | // Enable non-blocking mode. 92 | stream_set_blocking($this->fp, 0); 93 | 94 | return array("success" => true); 95 | } 96 | 97 | public function Stop() 98 | { 99 | if ($this->fp !== false) 100 | { 101 | foreach ($this->initclients as $id => $client) 102 | { 103 | fclose($client->fp); 104 | } 105 | 106 | foreach ($this->clients as $id => $client) 107 | { 108 | $this->RemoveClient($id); 109 | } 110 | 111 | fclose($this->fp); 112 | 113 | $this->initclients = array(); 114 | $this->clients = array(); 115 | $this->fp = false; 116 | $this->ssl = false; 117 | } 118 | 119 | $this->nextclientid = 1; 120 | } 121 | 122 | // Dangerous but allows for stream_select() calls on multiple, separate stream handles. 123 | public function GetStream() 124 | { 125 | return $this->fp; 126 | } 127 | 128 | public function UpdateStreamsAndTimeout($prefix, &$timeout, &$readfps, &$writefps) 129 | { 130 | if ($this->fp !== false) $readfps[$prefix . "gs_s"] = $this->fp; 131 | if ($timeout === false || $timeout > $this->defaulttimeout) $timeout = $this->defaulttimeout; 132 | 133 | foreach ($this->initclients as $id => $client) 134 | { 135 | if ($client->mode === "init") 136 | { 137 | $readfps[$prefix . "gs_c_" . $id] = $client->fp; 138 | if ($timeout > 1) $timeout = 1; 139 | } 140 | } 141 | foreach ($this->clients as $id => $client) 142 | { 143 | $readfps[$prefix . "gs_c_" . $id] = $client->fp; 144 | 145 | if ($client->writedata !== "") $writefps[$prefix . "gs_c_" . $id] = $client->fp; 146 | } 147 | } 148 | 149 | // Sometimes keyed arrays don't work properly. 150 | public static function FixedStreamSelect(&$readfps, &$writefps, &$exceptfps, $timeout) 151 | { 152 | // In order to correctly detect bad outputs, no '0' integer key is allowed. 153 | if (isset($readfps[0]) || isset($writefps[0]) || ($exceptfps !== NULL && isset($exceptfps[0]))) return false; 154 | 155 | $origreadfps = $readfps; 156 | $origwritefps = $writefps; 157 | $origexceptfps = $exceptfps; 158 | 159 | $result2 = stream_select($readfps, $writefps, $exceptfps, $timeout); 160 | if ($result2 === false) return false; 161 | 162 | if (isset($readfps[0])) 163 | { 164 | $fps = array(); 165 | foreach ($origreadfps as $key => $fp) $fps[(int)$fp] = $key; 166 | 167 | foreach ($readfps as $num => $fp) 168 | { 169 | $readfps[$fps[(int)$fp]] = $fp; 170 | 171 | unset($readfps[$num]); 172 | } 173 | } 174 | 175 | if (isset($writefps[0])) 176 | { 177 | $fps = array(); 178 | foreach ($origwritefps as $key => $fp) $fps[(int)$fp] = $key; 179 | 180 | foreach ($writefps as $num => $fp) 181 | { 182 | $writefps[$fps[(int)$fp]] = $fp; 183 | 184 | unset($writefps[$num]); 185 | } 186 | } 187 | 188 | if ($exceptfps !== NULL && isset($exceptfps[0])) 189 | { 190 | $fps = array(); 191 | foreach ($origexceptfps as $key => $fp) $fps[(int)$fp] = $key; 192 | 193 | foreach ($exceptfps as $num => $fp) 194 | { 195 | $exceptfps[$fps[(int)$fp]] = $fp; 196 | 197 | unset($exceptfps[$num]); 198 | } 199 | } 200 | 201 | return true; 202 | } 203 | 204 | public function InitNewClient($fp) 205 | { 206 | $client = new stdClass(); 207 | 208 | $client->id = $this->nextclientid; 209 | $client->mode = "init"; 210 | $client->readdata = ""; 211 | $client->writedata = ""; 212 | $client->recvsize = 0; 213 | $client->sendsize = 0; 214 | $client->lastts = microtime(true); 215 | $client->fp = $fp; 216 | $client->ipaddr = stream_socket_get_name($fp, true); 217 | 218 | $this->initclients[$this->nextclientid] = $client; 219 | 220 | $this->nextclientid++; 221 | 222 | return $client; 223 | } 224 | 225 | protected function HandleNewConnections(&$readfps, &$writefps) 226 | { 227 | if (isset($readfps["gs_s"])) 228 | { 229 | while (($fp = @stream_socket_accept($this->fp, 0)) !== false) 230 | { 231 | // Enable non-blocking mode. 232 | stream_set_blocking($fp, 0); 233 | 234 | $client = $this->InitNewClient($fp); 235 | 236 | if ($this->debug) echo "Accepted new connection from '" . $client->ipaddr . "'. Client ID " . $client->id . ".\n"; 237 | } 238 | 239 | unset($readfps["gs_s"]); 240 | } 241 | } 242 | 243 | private static function StreamTimedOut($fp) 244 | { 245 | if (!function_exists("stream_get_meta_data")) return false; 246 | 247 | $info = stream_get_meta_data($fp); 248 | 249 | return $info["timed_out"]; 250 | } 251 | 252 | private function ReadClientData($client) 253 | { 254 | $data2 = fread($client->fp, 65536); 255 | 256 | if ($data2 === false) return array("success" => false, "error" => self::GSTranslate("Underlying stream encountered a read error."), "errorcode" => "stream_read_error"); 257 | if ($data2 === "") 258 | { 259 | if (feof($client->fp)) return array("success" => false, "error" => self::GSTranslate("Remote peer disconnected."), "errorcode" => "peer_disconnected"); 260 | if (self::StreamTimedOut($client->fp)) return array("success" => false, "error" => self::GSTranslate("Underlying stream timed out."), "errorcode" => "stream_timeout_exceeded"); 261 | 262 | return array("success" => false, "error" => self::GSTranslate("Non-blocking read returned no data."), "errorcode" => "no_data"); 263 | } 264 | 265 | $tempsize = strlen($data2); 266 | $client->recvsize += $tempsize; 267 | 268 | $client->readdata .= $data2; 269 | 270 | return array("success" => true); 271 | } 272 | 273 | private function WriteClientData($client) 274 | { 275 | if ($client->writedata !== "") 276 | { 277 | // Serious bug in PHP core for non-blocking SSL sockets: https://bugs.php.net/bug.php?id=72333 278 | if ($this->ssl && version_compare(PHP_VERSION, "7.1.4") <= 0) 279 | { 280 | // This is a huge hack that has a pretty good chance of blocking on the socket. 281 | // Peeling off up to just 4KB at a time helps to minimize that possibility. It's better than guaranteed failure of the socket though. 282 | @stream_set_blocking($client->fp, 1); 283 | $result = fwrite($client->fp, (strlen($client->writedata) > 4096 ? substr($client->writedata, 0, 4096) : $client->writedata)); 284 | @stream_set_blocking($client->fp, 0); 285 | } 286 | else 287 | { 288 | $result = fwrite($client->fp, $client->writedata); 289 | } 290 | 291 | if ($result === false || feof($client->fp)) return array("success" => false, "error" => self::GSTranslate("A fwrite() failure occurred. Most likely cause: Connection failure."), "errorcode" => "fwrite_failed"); 292 | 293 | $data2 = substr($client->writedata, 0, $result); 294 | $client->writedata = (string)substr($client->writedata, $result); 295 | 296 | $client->sendsize += $result; 297 | 298 | $this->UpdateClientState($client->id); 299 | 300 | if (strlen($client->writedata)) return array("success" => false, "error" => self::GSTranslate("Non-blocking write did not send all data."), "errorcode" => "no_data"); 301 | } 302 | 303 | return array("success" => true); 304 | } 305 | 306 | // Handles new connections, the initial conversation, basic packet management, rate limits, and timeouts. 307 | // Can wait on more streams than just sockets and/or more sockets. Useful for waiting on other resources. 308 | // 'gs_s' and the 'gs_c_' prefix are reserved. 309 | // Returns an array of clients that may need more processing. 310 | public function Wait($timeout = false, $readfps = array(), $writefps = array(), $exceptfps = NULL) 311 | { 312 | $this->UpdateStreamsAndTimeout("", $timeout, $readfps, $writefps); 313 | 314 | $result = array("success" => true, "clients" => array(), "removed" => array(), "readfps" => array(), "writefps" => array(), "exceptfps" => array()); 315 | if (!count($readfps) && !count($writefps)) return $result; 316 | 317 | $result2 = self::FixedStreamSelect($readfps, $writefps, $exceptfps, $timeout); 318 | if ($result2 === false) return array("success" => false, "error" => self::GSTranslate("Wait() failed due to stream_select() failure. Most likely cause: Connection failure."), "errorcode" => "stream_select_failed"); 319 | 320 | // Return handles that were being waited on. 321 | $result["readfps"] = $readfps; 322 | $result["writefps"] = $writefps; 323 | $result["exceptfps"] = $exceptfps; 324 | 325 | $this->ProcessWaitResult($result); 326 | 327 | return $result; 328 | } 329 | 330 | protected function ProcessWaitResult(&$result) 331 | { 332 | // Handle new connections. 333 | $this->HandleNewConnections($result["readfps"], $result["writefps"]); 334 | 335 | // Handle clients in the read queue. 336 | foreach ($result["readfps"] as $cid => $fp) 337 | { 338 | if (!is_string($cid) || strlen($cid) < 6 || substr($cid, 0, 5) !== "gs_c_") continue; 339 | 340 | $id = (int)substr($cid, 5); 341 | 342 | if (!isset($this->clients[$id])) continue; 343 | 344 | $client = $this->clients[$id]; 345 | 346 | $client->lastts = microtime(true); 347 | 348 | $result2 = $this->ReadClientData($client); 349 | if ($result2["success"]) 350 | { 351 | // Let the caller know there is probably data to handle. 352 | $result["clients"][$id] = $client; 353 | } 354 | else if ($result2["errorcode"] !== "no_data") 355 | { 356 | if ($this->debug) echo "Read failed for client ID " . $client->id . ".\n"; 357 | 358 | $result["removed"][$id] = array("result" => $result2, "client" => $client); 359 | 360 | $this->RemoveClient($id); 361 | } 362 | 363 | unset($result["readfps"][$cid]); 364 | } 365 | 366 | // Handle clients in the write queue. 367 | foreach ($result["writefps"] as $cid => $fp) 368 | { 369 | if (!is_string($cid) || strlen($cid) < 6 || substr($cid, 0, 5) !== "gs_c_") continue; 370 | 371 | $id = (int)substr($cid, 5); 372 | 373 | if (!isset($this->clients[$id])) continue; 374 | 375 | $client = $this->clients[$id]; 376 | 377 | $client->lastts = microtime(true); 378 | 379 | $result2 = $this->WriteClientData($client); 380 | if ($result2["success"]) 381 | { 382 | // Let the caller add more data to the write buffer. 383 | $result["clients"][$id] = $client; 384 | } 385 | else if ($result2["errorcode"] !== "no_data") 386 | { 387 | if ($this->debug) echo "Write failed for client ID " . $client->id . ".\n"; 388 | 389 | $result["removed"][$id] = array("result" => $result2, "client" => $client); 390 | 391 | $this->RemoveClient($id); 392 | } 393 | 394 | unset($result["writefps"][$cid]); 395 | } 396 | 397 | // Initialize new clients. 398 | foreach ($this->initclients as $id => $client) 399 | { 400 | do 401 | { 402 | $origmode = $client->mode; 403 | 404 | switch ($client->mode) 405 | { 406 | case "init": 407 | { 408 | $result2 = ($this->ssl ? stream_socket_enable_crypto($client->fp, true, STREAM_CRYPTO_METHOD_TLSv1_1_SERVER | STREAM_CRYPTO_METHOD_TLSv1_2_SERVER) : true); 409 | 410 | if ($result2 === true) 411 | { 412 | $client->mode = "main"; 413 | 414 | if ($this->debug) echo "Switched to 'main' state for client ID " . $id . ".\n"; 415 | 416 | $client->lastts = microtime(true); 417 | 418 | $this->clients[$id] = $client; 419 | unset($this->initclients[$id]); 420 | 421 | $result["clients"][$id] = $client; 422 | } 423 | else if ($result2 === false) 424 | { 425 | if ($this->debug) echo "Unable to initialize crypto for client ID " . $id . ".\n"; 426 | 427 | fclose($client->fp); 428 | 429 | unset($this->initclients[$id]); 430 | } 431 | 432 | break; 433 | } 434 | } 435 | } while (isset($this->initclients[$id]) && $origmode !== $client->mode); 436 | } 437 | 438 | // Handle client timeouts. 439 | $ts = microtime(true); 440 | if ($this->lasttimeoutcheck <= $ts - 5) 441 | { 442 | foreach ($this->clients as $id => $client) 443 | { 444 | if ($client->lastts + $this->defaultclienttimeout < $ts) 445 | { 446 | if ($this->debug) echo "Client ID " . $id . " timed out. Removing.\n"; 447 | 448 | $result2 = array("success" => false, "error" => self::GSTranslate("Client timed out. Most likely cause: Connection failure."), "errorcode" => "client_timeout"); 449 | 450 | $result["removed"][$id] = array("result" => $result2, "client" => $client); 451 | 452 | $this->RemoveClient($id); 453 | } 454 | } 455 | 456 | $this->lasttimeoutcheck = $ts; 457 | } 458 | } 459 | 460 | public function GetClients() 461 | { 462 | return $this->clients; 463 | } 464 | 465 | public function NumClients() 466 | { 467 | return count($this->clients); 468 | } 469 | 470 | public function UpdateClientState($id) 471 | { 472 | } 473 | 474 | public function GetClient($id) 475 | { 476 | return (isset($this->clients[$id]) ? $this->clients[$id] : false); 477 | } 478 | 479 | public function DetachClient($id) 480 | { 481 | if (!isset($this->clients[$id])) return false; 482 | 483 | $client = $this->clients[$id]; 484 | 485 | unset($this->clients[$id]); 486 | 487 | return $client; 488 | } 489 | 490 | public function RemoveClient($id) 491 | { 492 | if (isset($this->clients[$id])) 493 | { 494 | $client = $this->clients[$id]; 495 | 496 | if ($client->fp !== false) fclose($client->fp); 497 | 498 | unset($this->clients[$id]); 499 | } 500 | } 501 | 502 | public static function GSTranslate() 503 | { 504 | $args = func_get_args(); 505 | if (!count($args)) return ""; 506 | 507 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 508 | } 509 | } 510 | ?> -------------------------------------------------------------------------------- /support/web_browser.php: -------------------------------------------------------------------------------- 1 | ResetState(); 15 | $this->SetState($prevstate); 16 | $this->html = false; 17 | } 18 | 19 | public function ResetState() 20 | { 21 | $this->data = array( 22 | "allowedprotocols" => array("http" => true, "https" => true), 23 | "allowedredirprotocols" => array("http" => true, "https" => true), 24 | "hostauths" => array(), 25 | "cookies" => array(), 26 | "referer" => "", 27 | "autoreferer" => true, 28 | "useragent" => "firefox", 29 | "followlocation" => true, 30 | "maxfollow" => 20, 31 | "extractforms" => false, 32 | "httpopts" => array(), 33 | ); 34 | } 35 | 36 | public function SetState($options = array()) 37 | { 38 | $this->data = array_merge($this->data, $options); 39 | } 40 | 41 | public function GetState() 42 | { 43 | return $this->data; 44 | } 45 | 46 | public function ProcessState(&$state) 47 | { 48 | while ($state["state"] !== "done") 49 | { 50 | switch ($state["state"]) 51 | { 52 | case "initialize": 53 | { 54 | if (!isset($this->data["allowedprotocols"][$state["urlinfo"]["scheme"]]) || !$this->data["allowedprotocols"][$state["urlinfo"]["scheme"]]) 55 | { 56 | return array("success" => false, "error" => self::WBTranslate("Protocol '%s' is not allowed in '%s'.", $state["urlinfo"]["scheme"], $state["url"]), "errorcode" => "allowed_protocols"); 57 | } 58 | 59 | $filename = HTTP::ExtractFilename($state["urlinfo"]["path"]); 60 | $pos = strrpos($filename, "."); 61 | $fileext = ($pos !== false ? strtolower(substr($filename, $pos + 1)) : ""); 62 | 63 | // Set up some standard headers. 64 | $headers = array(); 65 | $profile = strtolower($state["profile"]); 66 | $tempprofile = explode("-", $profile); 67 | if (count($tempprofile) == 2) 68 | { 69 | $profile = $tempprofile[0]; 70 | $fileext = $tempprofile[1]; 71 | } 72 | if (substr($profile, 0, 2) == "ie" || ($profile == "auto" && substr($this->data["useragent"], 0, 2) == "ie")) 73 | { 74 | if ($fileext == "css") $headers["Accept"] = "text/css"; 75 | else if ($fileext == "png" || $fileext == "jpg" || $fileext == "jpeg" || $fileext == "gif" || $fileext == "svg") $headers["Accept"] = "image/png, image/svg+xml, image/*;q=0.8, */*;q=0.5"; 76 | else if ($fileext == "js") $headers["Accept"] = "application/javascript, */*;q=0.8"; 77 | else if ($this->data["referer"] != "" || $fileext == "" || $fileext == "html" || $fileext == "xhtml" || $fileext == "xml") $headers["Accept"] = "text/html, application/xhtml+xml, */*"; 78 | else $headers["Accept"] = "*/*"; 79 | 80 | $headers["Accept-Language"] = "en-US"; 81 | $headers["User-Agent"] = HTTP::GetUserAgent(substr($profile, 0, 2) == "ie" ? $profile : $this->data["useragent"]); 82 | } 83 | else if ($profile == "firefox" || ($profile == "auto" && $this->data["useragent"] == "firefox")) 84 | { 85 | if ($fileext == "css") $headers["Accept"] = "text/css,*/*;q=0.1"; 86 | else if ($fileext == "png" || $fileext == "jpg" || $fileext == "jpeg" || $fileext == "gif" || $fileext == "svg") $headers["Accept"] = "image/png,image/*;q=0.8,*/*;q=0.5"; 87 | else if ($fileext == "js") $headers["Accept"] = "*/*"; 88 | else $headers["Accept"] = "text/html, application/xhtml+xml, */*"; 89 | 90 | $headers["Accept-Language"] = "en-us,en;q=0.5"; 91 | $headers["Cache-Control"] = "max-age=0"; 92 | $headers["User-Agent"] = HTTP::GetUserAgent("firefox"); 93 | } 94 | else if ($profile == "opera" || ($profile == "auto" && $this->data["useragent"] == "opera")) 95 | { 96 | // Opera has the right idea: Just send the same thing regardless of the request type. 97 | $headers["Accept"] = "text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/webp, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1"; 98 | $headers["Accept-Language"] = "en-US,en;q=0.9"; 99 | $headers["Cache-Control"] = "no-cache"; 100 | $headers["User-Agent"] = HTTP::GetUserAgent("opera"); 101 | } 102 | else if ($profile == "safari" || $profile == "edge" || $profile == "chrome" || ($profile == "auto" && ($this->data["useragent"] == "safari" || $this->data["useragent"] == "edge" || $this->data["useragent"] == "chrome"))) 103 | { 104 | if ($fileext == "css") $headers["Accept"] = "text/css,*/*;q=0.1"; 105 | else if ($fileext == "png" || $fileext == "jpg" || $fileext == "jpeg" || $fileext == "gif" || $fileext == "svg" || $fileext == "js") $headers["Accept"] = "*/*"; 106 | else $headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; 107 | 108 | $headers["Accept-Charset"] = "ISO-8859-1,utf-8;q=0.7,*;q=0.3"; 109 | $headers["Accept-Language"] = "en-US,en;q=0.8"; 110 | $headers["User-Agent"] = HTTP::GetUserAgent($profile == "safari" || $profile == "chrome" ? $profile : $this->data["useragent"]); 111 | } 112 | 113 | if ($this->data["referer"] != "") $headers["Referer"] = $this->data["referer"]; 114 | 115 | // Generate the final headers array. 116 | $headers = array_merge($headers, $state["httpopts"]["headers"], $state["tempoptions"]["headers"]); 117 | 118 | // Calculate the host and reverse host and remove port information. 119 | $host = (isset($headers["Host"]) ? $headers["Host"] : $state["urlinfo"]["host"]); 120 | $pos = strpos($host, "]"); 121 | if (substr($host, 0, 1) == "[" && $pos !== false) 122 | { 123 | $host = substr($host, 0, $pos + 1); 124 | } 125 | else 126 | { 127 | $pos = strpos($host, ":"); 128 | if ($pos !== false) $host = substr($host, 0, $pos); 129 | } 130 | $dothost = $host; 131 | $dothost = strtolower($dothost); 132 | if (substr($dothost, 0, 1) != ".") $dothost = "." . $dothost; 133 | $state["dothost"] = $dothost; 134 | 135 | // Append Authorization header. 136 | if (isset($headers["Authorization"])) $this->data["hostauths"][$host] = $headers["Authorization"]; 137 | else if (isset($this->data["hostauths"][$host])) $headers["Authorization"] = $this->data["hostauths"][$host]; 138 | 139 | // Append cookies and delete old, invalid cookies. 140 | $secure = ($state["urlinfo"]["scheme"] == "https"); 141 | $cookiepath = $state["urlinfo"]["path"]; 142 | if ($cookiepath == "") $cookiepath = "/"; 143 | $pos = strrpos($cookiepath, "/"); 144 | if ($pos !== false) $cookiepath = substr($cookiepath, 0, $pos + 1); 145 | $state["cookiepath"] = $cookiepath; 146 | $cookies = array(); 147 | foreach ($this->data["cookies"] as $domain => $paths) 148 | { 149 | if (strlen($dothost) >= strlen($domain) && substr($dothost, -strlen($domain)) === $domain) 150 | { 151 | foreach ($paths as $path => $cookies2) 152 | { 153 | if (substr($cookiepath, 0, strlen($path)) == $path) 154 | { 155 | foreach ($cookies2 as $num => $info) 156 | { 157 | if (isset($info["expires_ts"]) && $this->GetExpiresTimestamp($info["expires_ts"]) < time()) unset($this->data["cookies"][$domain][$path][$num]); 158 | else if ($secure || !isset($info["secure"])) $cookies[$info["name"]] = $info["value"]; 159 | } 160 | 161 | if (!count($this->data["cookies"][$domain][$path])) unset($this->data["cookies"][$domain][$path]); 162 | } 163 | } 164 | 165 | if (!count($this->data["cookies"][$domain])) unset($this->data["cookies"][$domain]); 166 | } 167 | } 168 | 169 | $cookies2 = array(); 170 | foreach ($cookies as $name => $value) $cookies2[] = rawurlencode($name) . "=" . rawurlencode($value); 171 | $headers["Cookie"] = implode("; ", $cookies2); 172 | if ($headers["Cookie"] == "") unset($headers["Cookie"]); 173 | 174 | // Generate the final options array. 175 | $state["options"] = array_merge($state["httpopts"], $state["tempoptions"]); 176 | $state["options"]["headers"] = $headers; 177 | if ($state["timeout"] !== false) $state["options"]["timeout"] = HTTP::GetTimeLeft($state["startts"], $state["timeout"]); 178 | 179 | // Let a callback handle any additional state changes. 180 | if (isset($state["options"]["pre_retrievewebpage_callback"]) && is_callable($state["options"]["pre_retrievewebpage_callback"]) && !call_user_func_array($state["options"]["pre_retrievewebpage_callback"], array(&$state))) 181 | { 182 | return array("success" => false, "error" => self::WBTranslate("Pre-RetrieveWebpage callback returned with a failure condition for '%s'.", $state["url"]), "errorcode" => "pre_retrievewebpage_callback"); 183 | } 184 | 185 | // Process the request. 186 | $result = HTTP::RetrieveWebpage($state["url"], $state["options"]); 187 | $result["url"] = $state["url"]; 188 | unset($state["options"]["files"]); 189 | unset($state["options"]["body"]); 190 | unset($state["tempoptions"]["headers"]["Content-Type"]); 191 | $result["options"] = $state["options"]; 192 | $result["firstreqts"] = $state["startts"]; 193 | $result["numredirects"] = $state["numredirects"]; 194 | $result["redirectts"] = $state["redirectts"]; 195 | if (isset($result["rawsendsize"])) $state["totalrawsendsize"] += $result["rawsendsize"]; 196 | $result["totalrawsendsize"] = $state["totalrawsendsize"]; 197 | if (!$result["success"]) return array("success" => false, "error" => self::WBTranslate("Unable to retrieve content. %s", $result["error"]), "info" => $result, "state" => $state, "errorcode" => "retrievewebpage"); 198 | 199 | if (isset($state["options"]["async"]) && $state["options"]["async"]) 200 | { 201 | $state["async"] = true; 202 | $state["httpstate"] = $result["state"]; 203 | 204 | $state["state"] = "process_async"; 205 | } 206 | else 207 | { 208 | $state["result"] = $result; 209 | 210 | $state["state"] = "post_retrieval"; 211 | } 212 | 213 | break; 214 | } 215 | case "process_async": 216 | { 217 | // Run a cycle of the HTTP state processor. 218 | $result = HTTP::ProcessState($state["httpstate"]); 219 | if (!$result["success"]) return $result; 220 | 221 | $result["url"] = $state["url"]; 222 | $result["options"] = $state["options"]; 223 | unset($result["options"]["files"]); 224 | unset($result["options"]["body"]); 225 | $result["firstreqts"] = $state["startts"]; 226 | $result["numredirects"] = $state["numredirects"]; 227 | $result["redirectts"] = $state["redirectts"]; 228 | if (isset($result["rawsendsize"])) $state["totalrawsendsize"] += $result["rawsendsize"]; 229 | $result["totalrawsendsize"] = $state["totalrawsendsize"]; 230 | 231 | $state["httpstate"] = false; 232 | $state["result"] = $result; 233 | 234 | $state["state"] = "post_retrieval"; 235 | 236 | break; 237 | } 238 | case "post_retrieval": 239 | { 240 | // Set up structures for another round. 241 | if ($this->data["autoreferer"]) $this->data["referer"] = $state["url"]; 242 | if (isset($state["result"]["headers"]["Location"]) && $this->data["followlocation"]) 243 | { 244 | $state["redirectts"] = microtime(true); 245 | 246 | unset($state["tempoptions"]["method"]); 247 | unset($state["tempoptions"]["write_body_callback"]); 248 | unset($state["tempoptions"]["body"]); 249 | unset($state["tempoptions"]["postvars"]); 250 | unset($state["tempoptions"]["files"]); 251 | 252 | $state["tempoptions"]["headers"]["Referer"] = $state["url"]; 253 | $state["url"] = $state["result"]["headers"]["Location"][0]; 254 | 255 | // Generate an absolute URL. 256 | if ($this->data["referer"] != "") $state["url"] = HTTP::ConvertRelativeToAbsoluteURL($this->data["referer"], $state["url"]); 257 | 258 | $urlinfo2 = HTTP::ExtractURL($state["url"]); 259 | 260 | if (!isset($this->data["allowedredirprotocols"][$urlinfo2["scheme"]]) || !$this->data["allowedredirprotocols"][$urlinfo2["scheme"]]) 261 | { 262 | return array("success" => false, "error" => self::WBTranslate("Protocol '%s' is not allowed. Server attempted to redirect to '%s'.", $urlinfo2["scheme"], $state["url"]), "info" => $state["result"], "errorcode" => "allowed_redir_protocols"); 263 | } 264 | 265 | if ($urlinfo2["host"] != $state["urlinfo"]["host"]) 266 | { 267 | unset($state["tempoptions"]["headers"]["Host"]); 268 | unset($state["httpopts"]["headers"]["Host"]); 269 | 270 | unset($state["httpopts"]["headers"]["Authorization"]); 271 | unset($state["tempoptions"]["headers"]["Authorization"]); 272 | } 273 | 274 | $state["urlinfo"] = $urlinfo2; 275 | $state["numredirects"]++; 276 | } 277 | 278 | // Handle any 'Set-Cookie' headers. 279 | if (isset($state["result"]["headers"]["Set-Cookie"])) 280 | { 281 | foreach ($state["result"]["headers"]["Set-Cookie"] as $cookie) 282 | { 283 | $items = explode(";", $cookie); 284 | $item = trim(array_shift($items)); 285 | if ($item != "") 286 | { 287 | $cookie2 = array(); 288 | $pos = strpos($item, "="); 289 | if ($pos === false) 290 | { 291 | $cookie2["name"] = urldecode($item); 292 | $cookie2["value"] = ""; 293 | } 294 | else 295 | { 296 | $cookie2["name"] = urldecode(substr($item, 0, $pos)); 297 | $cookie2["value"] = urldecode(substr($item, $pos + 1)); 298 | } 299 | 300 | $cookie = array(); 301 | foreach ($items as $item) 302 | { 303 | $item = trim($item); 304 | if ($item != "") 305 | { 306 | $pos = strpos($item, "="); 307 | if ($pos === false) $cookie[strtolower(trim(urldecode($item)))] = ""; 308 | else $cookie[strtolower(trim(urldecode(substr($item, 0, $pos))))] = urldecode(substr($item, $pos + 1)); 309 | } 310 | } 311 | $cookie = array_merge($cookie, $cookie2); 312 | 313 | if (isset($cookie["expires"])) 314 | { 315 | $ts = HTTP::GetDateTimestamp($cookie["expires"]); 316 | $cookie["expires_ts"] = gmdate("Y-m-d H:i:s", ($ts === false ? time() - 24 * 60 * 60 : $ts)); 317 | } 318 | else if (isset($cookie["max-age"])) 319 | { 320 | $cookie["expires_ts"] = gmdate("Y-m-d H:i:s", time() + (int)$cookie["max-age"]); 321 | } 322 | else 323 | { 324 | unset($cookie["expires_ts"]); 325 | } 326 | 327 | if (!isset($cookie["domain"])) $cookie["domain"] = $state["dothost"]; 328 | if (!isset($cookie["path"])) $cookie["path"] = $state["cookiepath"]; 329 | 330 | $this->SetCookie($cookie); 331 | } 332 | } 333 | } 334 | 335 | if ($state["numfollow"] > 0) $state["numfollow"]--; 336 | 337 | // If this is a redirect, handle it by starting over. 338 | if (isset($state["result"]["headers"]["Location"]) && $this->data["followlocation"] && $state["numfollow"]) 339 | { 340 | $state["result"] = false; 341 | 342 | $state["state"] = "initialize"; 343 | } 344 | else 345 | { 346 | $state["result"]["numredirects"] = $state["numredirects"]; 347 | $state["result"]["redirectts"] = $state["redirectts"]; 348 | 349 | // Extract the forms from the page in a parsed format. 350 | // Call WebBrowser::GenerateFormRequest() to prepare an actual request for Process(). 351 | if ($this->data["extractforms"]) $state["result"]["forms"] = $this->ExtractForms($state["result"]["url"], $state["result"]["body"], (isset($state["tempoptions"]["extractforms_hint"]) ? $state["tempoptions"]["extractforms_hint"] : false)); 352 | 353 | $state["state"] = "done"; 354 | } 355 | 356 | break; 357 | } 358 | } 359 | } 360 | 361 | return $state["result"]; 362 | } 363 | 364 | public function Process($url, $tempoptions = array()) 365 | { 366 | $startts = microtime(true); 367 | $redirectts = $startts; 368 | 369 | // Handle older function call: Process($url, $profile, $tempoptions) 370 | if (is_string($tempoptions)) 371 | { 372 | $args = func_get_args(); 373 | if (count($args) < 3) $tempoptions = array(); 374 | else $tempoptions = $args[2]; 375 | 376 | $tempoptions["profile"] = $args[1]; 377 | } 378 | 379 | $profile = (isset($tempoptions["profile"]) ? $tempoptions["profile"] : "auto"); 380 | 381 | if (isset($tempoptions["timeout"])) $timeout = $tempoptions["timeout"]; 382 | else if (isset($this->data["httpopts"]["timeout"])) $timeout = $this->data["httpopts"]["timeout"]; 383 | else $timeout = false; 384 | 385 | // Deal with possible application hanging issues. 386 | if (isset($tempoptions["streamtimeout"])) $streamtimeout = $tempoptions["streamtimeout"]; 387 | else if (isset($this->data["httpopts"]["streamtimeout"])) $streamtimeout = $this->data["httpopts"]["streamtimeout"]; 388 | else $streamtimeout = 300; 389 | $tempoptions["streamtimeout"] = $streamtimeout; 390 | 391 | if (!isset($this->data["httpopts"]["headers"])) $this->data["httpopts"]["headers"] = array(); 392 | $this->data["httpopts"]["headers"] = HTTP::NormalizeHeaders($this->data["httpopts"]["headers"]); 393 | unset($this->data["httpopts"]["method"]); 394 | unset($this->data["httpopts"]["write_body_callback"]); 395 | unset($this->data["httpopts"]["body"]); 396 | unset($this->data["httpopts"]["postvars"]); 397 | unset($this->data["httpopts"]["files"]); 398 | 399 | $httpopts = $this->data["httpopts"]; 400 | $numfollow = $this->data["maxfollow"]; 401 | $numredirects = 0; 402 | $totalrawsendsize = 0; 403 | 404 | if (!isset($tempoptions["headers"])) $tempoptions["headers"] = array(); 405 | $tempoptions["headers"] = HTTP::NormalizeHeaders($tempoptions["headers"]); 406 | if (isset($tempoptions["headers"]["Referer"])) $this->data["referer"] = $tempoptions["headers"]["Referer"]; 407 | 408 | // If a referrer is specified, use it to generate an absolute URL. 409 | if ($this->data["referer"] != "") $url = HTTP::ConvertRelativeToAbsoluteURL($this->data["referer"], $url); 410 | 411 | $urlinfo = HTTP::ExtractURL($url); 412 | 413 | // Initialize the process state array. 414 | $state = array( 415 | "async" => false, 416 | "startts" => $startts, 417 | "redirectts" => $redirectts, 418 | "timeout" => $timeout, 419 | "tempoptions" => $tempoptions, 420 | "httpopts" => $httpopts, 421 | "numfollow" => $numfollow, 422 | "numredirects" => $numredirects, 423 | "totalrawsendsize" => $totalrawsendsize, 424 | "profile" => $profile, 425 | "url" => $url, 426 | "urlinfo" => $urlinfo, 427 | 428 | "state" => "initialize", 429 | "httpstate" => false, 430 | "result" => false, 431 | ); 432 | 433 | // Run at least one state cycle to properly initialize the state array. 434 | $result = $this->ProcessState($state); 435 | 436 | // Return the state for async calls. Caller must call ProcessState(). 437 | if ($state["async"]) return array("success" => true, "state" => $state); 438 | 439 | return $result; 440 | } 441 | 442 | // Implements the correct MultiAsyncHelper responses for WebBrowser instances. 443 | public function ProcessAsync__Handler($mode, &$data, $key, &$info) 444 | { 445 | switch ($mode) 446 | { 447 | case "init": 448 | { 449 | if ($info["init"]) $data = $info["keep"]; 450 | else 451 | { 452 | $info["result"] = $this->Process($info["url"], $info["tempoptions"]); 453 | if (!$info["result"]["success"]) 454 | { 455 | $info["keep"] = false; 456 | 457 | if (is_callable($info["callback"])) call_user_func_array($info["callback"], array($key, $info["url"], $info["result"])); 458 | } 459 | else 460 | { 461 | $info["state"] = $info["result"]["state"]; 462 | 463 | // Move to the live queue. 464 | $data = true; 465 | } 466 | } 467 | 468 | break; 469 | } 470 | case "update": 471 | case "read": 472 | case "write": 473 | { 474 | if ($info["keep"]) 475 | { 476 | $info["result"] = $this->ProcessState($info["state"]); 477 | if ($info["result"]["success"] || $info["result"]["errorcode"] !== "no_data") $info["keep"] = false; 478 | 479 | if (is_callable($info["callback"])) call_user_func_array($info["callback"], array($key, $info["url"], $info["result"])); 480 | 481 | if ($mode === "update") $data = $info["keep"]; 482 | } 483 | 484 | break; 485 | } 486 | case "readfps": 487 | { 488 | if ($info["state"]["httpstate"] !== false && HTTP::WantRead($info["state"]["httpstate"])) $data[$key] = $info["state"]["httpstate"]["fp"]; 489 | 490 | break; 491 | } 492 | case "writefps": 493 | { 494 | if ($info["state"]["httpstate"] !== false && HTTP::WantWrite($info["state"]["httpstate"])) $data[$key] = $info["state"]["httpstate"]["fp"]; 495 | 496 | break; 497 | } 498 | case "cleanup": 499 | { 500 | // When true, caller is removing. Otherwise, detaching from the queue. 501 | if ($data === true) 502 | { 503 | if (isset($info["state"])) 504 | { 505 | if ($info["state"]["httpstate"] !== false) HTTP::ForceClose($info["state"]["httpstate"]); 506 | 507 | unset($info["state"]); 508 | } 509 | 510 | $info["keep"] = false; 511 | } 512 | 513 | break; 514 | } 515 | } 516 | } 517 | 518 | public function ProcessAsync($helper, $key, $callback, $url, $tempoptions = array()) 519 | { 520 | $tempoptions["async"] = true; 521 | 522 | // Handle older function call: ProcessAsync($helper, $key, $callback, $url, $profile, $tempoptions) 523 | if (is_string($tempoptions)) 524 | { 525 | $args = func_get_args(); 526 | if (count($args) < 6) $tempoptions = array(); 527 | else $tempoptions = $args[5]; 528 | 529 | $tempoptions["profile"] = $args[4]; 530 | } 531 | 532 | $profile = (isset($tempoptions["profile"]) ? $tempoptions["profile"] : "auto"); 533 | 534 | $info = array( 535 | "init" => false, 536 | "keep" => true, 537 | "callback" => $callback, 538 | "url" => $url, 539 | "tempoptions" => $tempoptions, 540 | "result" => false 541 | ); 542 | 543 | $helper->Set($key, $info, array($this, "ProcessAsync__Handler")); 544 | 545 | return array("success" => true); 546 | } 547 | 548 | public function ExtractForms($baseurl, $data, $hint = false) 549 | { 550 | $result = array(); 551 | 552 | $lasthint = ""; 553 | $hintmap = array(); 554 | if ($this->html === false) 555 | { 556 | if (!class_exists("simple_html_dom", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/simple_html_dom.php"; 557 | 558 | $this->html = new simple_html_dom(); 559 | } 560 | $this->html->load($data); 561 | $rows = $this->html->find("label[for]"); 562 | foreach ($rows as $row) 563 | { 564 | $hintmap[trim($row->for)] = trim($row->plaintext); 565 | } 566 | $html5rows = $this->html->find("input[form],textarea[form],select[form],button[form],datalist[id]" . ($hint !== false ? "," . $hint : "")); 567 | $rows = $this->html->find("form"); 568 | foreach ($rows as $row) 569 | { 570 | $info = array(); 571 | if (isset($row->id)) $info["id"] = trim($row->id); 572 | if (isset($row->name)) $info["name"] = (string)$row->name; 573 | $info["action"] = (isset($row->action) ? HTTP::ConvertRelativeToAbsoluteURL($baseurl, (string)$row->action) : $baseurl); 574 | $info["method"] = (isset($row->method) && strtolower(trim($row->method)) == "post" ? "post" : "get"); 575 | if ($info["method"] == "post") $info["enctype"] = (isset($row->enctype) ? strtolower($row->enctype) : "application/x-www-form-urlencoded"); 576 | if (isset($row->{"accept-charset"})) $info["accept-charset"] = (string)$row->{"accept-charset"}; 577 | 578 | $fields = array(); 579 | $rows2 = $row->find("input,textarea,select,button" . ($hint !== false ? "," . $hint : "")); 580 | foreach ($rows2 as $row2) 581 | { 582 | if (!isset($row2->form)) 583 | { 584 | if (isset($row2->id) && $row2->id != "" && isset($hintmap[trim($row2->id)])) $lasthint = $hintmap[trim($row2->id)]; 585 | 586 | $this->ExtractFieldFromDOM($fields, $row2, $lasthint); 587 | } 588 | } 589 | 590 | // Handle HTML5. 591 | if (isset($info["id"]) && $info["id"] != "") 592 | { 593 | foreach ($html5rows as $row2) 594 | { 595 | if (strpos(" " . $info["id"] . " ", " " . $row2->form . " ") !== false) 596 | { 597 | if (isset($hintmap[$info["id"]])) $lasthint = $hintmap[$info["id"]]; 598 | 599 | $this->ExtractFieldFromDOM($fields, $row2, $lasthint); 600 | } 601 | } 602 | } 603 | 604 | $form = new WebBrowserForm(); 605 | $form->info = $info; 606 | $form->fields = $fields; 607 | $result[] = $form; 608 | } 609 | 610 | return $result; 611 | } 612 | 613 | private function ExtractFieldFromDOM(&$fields, $row, &$lasthint) 614 | { 615 | switch ($row->tag) 616 | { 617 | case "input": 618 | { 619 | if (!isset($row->name) && ($row->type === "submit" || $row->type === "image")) $row->name = ""; 620 | 621 | if (isset($row->name) && is_string($row->name)) 622 | { 623 | $field = array( 624 | "id" => (isset($row->id) ? (string)$row->id : false), 625 | "type" => "input." . (isset($row->type) ? strtolower($row->type) : "text"), 626 | "name" => $row->name, 627 | "value" => (isset($row->value) ? html_entity_decode($row->value, ENT_COMPAT, "UTF-8") : "") 628 | ); 629 | if ($field["type"] == "input.radio" || $field["type"] == "input.checkbox") 630 | { 631 | $field["checked"] = (isset($row->checked)); 632 | 633 | if ($field["value"] === "") $field["value"] = "on"; 634 | } 635 | 636 | if (isset($row->placeholder)) $field["hint"] = trim($row->placeholder); 637 | else if ($field["type"] == "input.submit" || $field["type"] == "input.image") $field["hint"] = $field["type"] . "|" . $field["value"]; 638 | else if ($lasthint !== "") $field["hint"] = $lasthint; 639 | 640 | $fields[] = $field; 641 | 642 | $lasthint = ""; 643 | } 644 | 645 | break; 646 | } 647 | case "textarea": 648 | { 649 | if (isset($row->name) && is_string($row->name)) 650 | { 651 | $field = array( 652 | "id" => (isset($row->id) ? (string)$row->id : false), 653 | "type" => "textarea", 654 | "name" => $row->name, 655 | "value" => html_entity_decode($row->innertext, ENT_COMPAT, "UTF-8") 656 | ); 657 | 658 | if (isset($row->placeholder)) $field["hint"] = trim($row->placeholder); 659 | else if ($lasthint !== "") $field["hint"] = $lasthint; 660 | 661 | $fields[] = $field; 662 | 663 | $lasthint = ""; 664 | } 665 | 666 | break; 667 | } 668 | case "select": 669 | { 670 | if (isset($row->name) && is_string($row->name)) 671 | { 672 | if (isset($row->multiple)) 673 | { 674 | // Change the type into multiple checkboxes. 675 | $rows = $row->find("option"); 676 | foreach ($rows as $row2) 677 | { 678 | $field = array( 679 | "id" => (isset($row->id) ? (string)$row->id : false), 680 | "type" => "input.checkbox", 681 | "name" => $row->name, 682 | "value" => (isset($row2->value) ? html_entity_decode($row2->value, ENT_COMPAT, "UTF-8") : ""), 683 | "display" => (string)$row2->innertext 684 | ); 685 | if ($lasthint !== "") $field["hint"] = $lasthint; 686 | 687 | $fields[] = $field; 688 | } 689 | } 690 | else 691 | { 692 | $val = false; 693 | $options = array(); 694 | $rows = $row->find("option"); 695 | foreach ($rows as $row2) 696 | { 697 | $options[$row2->value] = (string)$row2->innertext; 698 | 699 | if ($val === false && isset($row2->selected)) $val = html_entity_decode($row2->value, ENT_COMPAT, "UTF-8"); 700 | } 701 | if ($val === false && count($options)) 702 | { 703 | $val = array_keys($options); 704 | $val = $val[0]; 705 | } 706 | if ($val === false) $val = ""; 707 | 708 | $field = array( 709 | "id" => (isset($row->id) ? (string)$row->id : false), 710 | "type" => "select", 711 | "name" => $row->name, 712 | "value" => $val, 713 | "options" => $options 714 | ); 715 | if ($lasthint !== "") $field["hint"] = $lasthint; 716 | 717 | $fields[] = $field; 718 | } 719 | 720 | $lasthint = ""; 721 | } 722 | 723 | break; 724 | } 725 | case "button": 726 | { 727 | if (isset($row->name) && is_string($row->name)) 728 | { 729 | $field = array( 730 | "id" => (isset($row->id) ? (string)$row->id : false), 731 | "type" => "button." . (isset($row->type) ? strtolower($row->type) : "submit"), 732 | "name" => $row->name, 733 | "value" => (isset($row->value) ? html_entity_decode($row->value, ENT_COMPAT, "UTF-8") : "") 734 | ); 735 | $field["hint"] = (trim($row->plaintext) !== "" ? trim($row->plaintext) : "button|" . $field["value"]); 736 | 737 | $fields[] = $field; 738 | 739 | $lasthint = ""; 740 | } 741 | 742 | break; 743 | } 744 | case "datalist": 745 | { 746 | // Do nothing since browsers don't actually enforce this tag's values. 747 | 748 | break; 749 | } 750 | default: 751 | { 752 | // Hint for the next element. 753 | $lasthint = (string)$row->plaintext; 754 | 755 | break; 756 | } 757 | } 758 | } 759 | 760 | public static function InteractiveFormFill($forms, $showselected = false) 761 | { 762 | if (!is_array($forms)) $forms = array($forms); 763 | 764 | if (!count($forms)) return false; 765 | 766 | if (count($forms) == 1) $form = reset($forms); 767 | else 768 | { 769 | echo self::WBTranslate("There are multiple forms available to fill out:\n"); 770 | foreach ($forms as $num => $form) 771 | { 772 | echo self::WBTranslate("\t%d:\n", $num + 1); 773 | foreach ($form->info as $key => $val) echo self::WBTranslate("\t\t%s: %s\n", $key, $val); 774 | echo self::WBTranslate("\t\tfields: %d\n", count($form->GetVisibleFields(false))); 775 | echo self::WBTranslate("\t\tbuttons: %d\n", count($form->GetVisibleFields(true)) - count($form->GetVisibleFields(false))); 776 | echo "\n"; 777 | } 778 | 779 | do 780 | { 781 | echo self::WBTranslate("Select: "); 782 | 783 | $num = (int)trim(fgets(STDIN)) - 1; 784 | } while (!isset($forms[$num])); 785 | 786 | $form = $forms[$num]; 787 | } 788 | 789 | if ($showselected) 790 | { 791 | echo self::WBTranslate("Selected form:\n"); 792 | foreach ($form->info as $key => $val) echo self::WBTranslate("\t%s: %s\n", $key, $val); 793 | echo "\n"; 794 | } 795 | 796 | if (count($form->GetVisibleFields(false))) 797 | { 798 | echo self::WBTranslate("Select form fields by field number to edit a field. When ready to submit the form, leave 'Field number' empty.\n\n"); 799 | 800 | do 801 | { 802 | echo self::WBTranslate("Editable form fields:\n"); 803 | foreach ($form->fields as $num => $field) 804 | { 805 | if ($field["type"] == "input.hidden" || $field["type"] == "input.submit" || $field["type"] == "input.image" || $field["type"] == "input.button" || substr($field["type"], 0, 7) == "button.") continue; 806 | 807 | echo self::WBTranslate("\t%d: %s - %s\n", $num + 1, $field["name"], (is_array($field["value"]) ? json_encode($field["value"], JSON_PRETTY_PRINT) : $field["value"]) . (($field["type"] == "input.radio" || $field["type"] == "input.checkbox") ? ($field["checked"] ? self::WBTranslate(" [Y]") : self::WBTranslate(" [N]")) : "") . (isset($field["hint"]) && $field["hint"] !== "" ? " [" . $field["hint"] . "]" : "")); 808 | } 809 | echo "\n"; 810 | 811 | do 812 | { 813 | echo self::WBTranslate("Field number: "); 814 | 815 | $num = trim(fgets(STDIN)); 816 | if ($num === "") break; 817 | 818 | $num = (int)$num - 1; 819 | } while (!isset($form->fields[$num]) || $form->fields[$num]["type"] == "input.hidden" || $form->fields[$num]["type"] == "input.submit" || $form->fields[$num]["type"] == "input.image" || $form->fields[$num]["type"] == "input.button" || substr($form->fields[$num]["type"], 0, 7) == "button."); 820 | 821 | if ($num === "") 822 | { 823 | echo "\n"; 824 | 825 | break; 826 | } 827 | 828 | $field = $form->fields[$num]; 829 | $prefix = (isset($field["hint"]) && $field["hint"] !== "" ? $field["hint"] . " | " : "") . $field["name"]; 830 | 831 | if ($field["type"] == "select") 832 | { 833 | echo self::WBTranslate("[%s] Options:\n", $prefix); 834 | foreach ($field["options"] as $key => $val) 835 | { 836 | echo self::WBTranslate("\t%s: %s\n"); 837 | } 838 | 839 | do 840 | { 841 | echo self::WBTranslate("[%s] Select: ", $prefix); 842 | 843 | $select = rtrim(fgets(STDIN)); 844 | } while (!isset($field["options"][$select])); 845 | 846 | $form->fields[$num]["value"] = $select; 847 | } 848 | else if ($field["type"] == "input.radio") 849 | { 850 | $form->SetFormValue($field["name"], $field["value"], true, "input.radio"); 851 | } 852 | else if ($field["type"] == "input.checkbox") 853 | { 854 | $form->fields[$num]["checked"] = !$field["checked"]; 855 | } 856 | else if ($field["type"] == "input.file") 857 | { 858 | do 859 | { 860 | echo self::WBTranslate("[%s] Filename: ", $prefix); 861 | 862 | $filename = rtrim(fgets(STDIN)); 863 | } while ($filename !== "" && !file_exists($filename)); 864 | 865 | if ($filename === "") $form->fields[$num]["value"] = ""; 866 | else 867 | { 868 | $form->fields[$num]["value"] = array( 869 | "filename" => $filename, 870 | "type" => "application/octet-stream", 871 | "datafile" => $filename 872 | ); 873 | } 874 | } 875 | else 876 | { 877 | echo self::WBTranslate("[%s] New value: ", $prefix); 878 | 879 | $form->fields[$num]["value"] = rtrim(fgets(STDIN)); 880 | } 881 | 882 | echo "\n"; 883 | 884 | } while (1); 885 | } 886 | 887 | $submitoptions = array(array("name" => self::WBTranslate("Default action"), "value" => self::WBTranslate("Might not work"), "hint" => "Default action")); 888 | foreach ($form->fields as $num => $field) 889 | { 890 | if ($field["type"] != "input.submit" && $field["type"] != "input.image" && $field["type"] != "input.button" && $field["type"] != "button.submit") continue; 891 | 892 | $submitoptions[] = $field; 893 | } 894 | 895 | if (count($submitoptions) <= 2) $num = count($submitoptions) - 1; 896 | else 897 | { 898 | echo self::WBTranslate("Available submit buttons:\n"); 899 | foreach ($submitoptions as $num => $field) 900 | { 901 | echo self::WBTranslate("\t%d: %s - %s\n", $num, $field["name"], $field["value"] . (isset($field["hint"]) && $field["hint"] !== "" ? " [" . $field["hint"] . "]" : "")); 902 | } 903 | echo "\n"; 904 | 905 | do 906 | { 907 | echo self::WBTranslate("Select: "); 908 | 909 | $num = (int)fgets(STDIN); 910 | } while (!isset($submitoptions[$num])); 911 | 912 | echo "\n"; 913 | } 914 | 915 | $result = $form->GenerateFormRequest(($num ? $submitoptions[$num]["name"] : false), ($num ? $submitoptions[$num]["value"] : false)); 916 | 917 | return $result; 918 | } 919 | 920 | public function GetCookies() 921 | { 922 | return $this->data["cookies"]; 923 | } 924 | 925 | public function SetCookie($cookie) 926 | { 927 | if (!isset($cookie["domain"]) || !isset($cookie["path"]) || !isset($cookie["name"]) || !isset($cookie["value"])) return array("success" => false, "error" => self::WBTranslate("SetCookie() requires 'domain', 'path', 'name', and 'value' to be options."), "errorcode" => "missing_information"); 928 | 929 | $cookie["domain"] = strtolower($cookie["domain"]); 930 | if (substr($cookie["domain"], 0, 1) != ".") $cookie["domain"] = "." . $cookie["domain"]; 931 | 932 | $cookie["path"] = str_replace("\\", "/", $cookie["path"]); 933 | if (substr($cookie["path"], -1) != "/") $cookie["path"] = "/"; 934 | 935 | if (!isset($this->data["cookies"][$cookie["domain"]])) $this->data["cookies"][$cookie["domain"]] = array(); 936 | if (!isset($this->data["cookies"][$cookie["domain"]][$cookie["path"]])) $this->data["cookies"][$cookie["domain"]][$cookie["path"]] = array(); 937 | $this->data["cookies"][$cookie["domain"]][$cookie["path"]][$cookie["name"]] = $cookie; 938 | 939 | return array("success" => true); 940 | } 941 | 942 | // Simulates closing a web browser. 943 | public function DeleteSessionCookies() 944 | { 945 | foreach ($this->data["cookies"] as $domain => $paths) 946 | { 947 | foreach ($paths as $path => $cookies) 948 | { 949 | foreach ($cookies as $num => $info) 950 | { 951 | if (!isset($info["expires_ts"])) unset($this->data["cookies"][$domain][$path][$num]); 952 | } 953 | 954 | if (!count($this->data["cookies"][$domain][$path])) unset($this->data["cookies"][$domain][$path]); 955 | } 956 | 957 | if (!count($this->data["cookies"][$domain])) unset($this->data["cookies"][$domain]); 958 | } 959 | } 960 | 961 | public function DeleteCookies($domainpattern, $pathpattern, $namepattern) 962 | { 963 | foreach ($this->data["cookies"] as $domain => $paths) 964 | { 965 | if ($domainpattern == "" || substr($domain, -strlen($domainpattern)) == $domainpattern) 966 | { 967 | foreach ($paths as $path => $cookies) 968 | { 969 | if ($pathpattern == "" || substr($path, 0, strlen($pathpattern)) == $pathpattern) 970 | { 971 | foreach ($cookies as $num => $info) 972 | { 973 | if ($namepattern == "" || strpos($info["name"], $namepattern) !== false) unset($this->data["cookies"][$domain][$path][$num]); 974 | } 975 | 976 | if (!count($this->data["cookies"][$domain][$path])) unset($this->data["cookies"][$domain][$path]); 977 | } 978 | } 979 | 980 | if (!count($this->data["cookies"][$domain])) unset($this->data["cookies"][$domain]); 981 | } 982 | } 983 | } 984 | 985 | private function GetExpiresTimestamp($ts) 986 | { 987 | $year = (int)substr($ts, 0, 4); 988 | $month = (int)substr($ts, 5, 2); 989 | $day = (int)substr($ts, 8, 2); 990 | $hour = (int)substr($ts, 11, 2); 991 | $min = (int)substr($ts, 14, 2); 992 | $sec = (int)substr($ts, 17, 2); 993 | 994 | return gmmktime($hour, $min, $sec, $month, $day, $year); 995 | } 996 | 997 | public static function WBTranslate() 998 | { 999 | $args = func_get_args(); 1000 | if (!count($args)) return ""; 1001 | 1002 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 1003 | } 1004 | } 1005 | 1006 | class WebBrowserForm 1007 | { 1008 | public $info, $fields; 1009 | 1010 | public function __construct() 1011 | { 1012 | $this->info = array(); 1013 | $this->fields = array(); 1014 | } 1015 | 1016 | public function FindFormFields($name = false, $value = false, $type = false) 1017 | { 1018 | $fields = array(); 1019 | foreach ($this->fields as $num => $field) 1020 | { 1021 | if (($type === false || $field["type"] === $type) && ($name === false || $field["name"] === $name) && ($value === false || $field["value"] === $value)) 1022 | { 1023 | $fields[] = $field; 1024 | } 1025 | } 1026 | 1027 | return $fields; 1028 | } 1029 | 1030 | public function GetHintMap() 1031 | { 1032 | $result = array(); 1033 | foreach ($this->fields as $num => $field) 1034 | { 1035 | if (isset($field["hint"])) $result[$field["hint"]] = $field["name"]; 1036 | } 1037 | 1038 | return $result; 1039 | } 1040 | 1041 | public function GetVisibleFields($submit) 1042 | { 1043 | $result = array(); 1044 | foreach ($this->fields as $num => $field) 1045 | { 1046 | if ($field["type"] == "input.hidden" || (!$submit && ($field["type"] == "input.submit" || $field["type"] == "input.image" || $field["type"] == "input.button" || substr($field["type"], 0, 7) == "button."))) continue; 1047 | 1048 | $result[$num] = $field; 1049 | } 1050 | 1051 | return $result; 1052 | } 1053 | 1054 | public function GetFormValue($name, $checkval = false, $type = false) 1055 | { 1056 | $val = false; 1057 | foreach ($this->fields as $field) 1058 | { 1059 | if (($type === false || $field["type"] === $type) && $field["name"] === $name) 1060 | { 1061 | if (is_string($checkval)) 1062 | { 1063 | if ($checkval === $field["value"]) 1064 | { 1065 | if ($field["type"] == "input.radio" || $field["type"] == "input.checkbox") $val = $field["checked"]; 1066 | else $val = $field["value"]; 1067 | } 1068 | } 1069 | else if (($field["type"] != "input.radio" && $field["type"] != "input.checkbox") || $field["checked"]) 1070 | { 1071 | $val = $field["value"]; 1072 | } 1073 | } 1074 | } 1075 | 1076 | return $val; 1077 | } 1078 | 1079 | public function SetFormValue($name, $value, $checked = false, $type = false, $create = false) 1080 | { 1081 | $result = false; 1082 | foreach ($this->fields as $num => $field) 1083 | { 1084 | if (($type === false || $field["type"] === $type) && $field["name"] === $name) 1085 | { 1086 | if ($field["type"] == "input.radio") 1087 | { 1088 | $this->fields[$num]["checked"] = ($field["value"] === $value ? $checked : false); 1089 | $result = true; 1090 | } 1091 | else if ($field["type"] == "input.checkbox") 1092 | { 1093 | if ($field["value"] === $value) $this->fields[$num]["checked"] = $checked; 1094 | $result = true; 1095 | } 1096 | else if ($field["type"] != "select" || !isset($field["options"]) || isset($field["options"][$value])) 1097 | { 1098 | $this->fields[$num]["value"] = $value; 1099 | $result = true; 1100 | } 1101 | } 1102 | } 1103 | 1104 | // Add the field if it doesn't exist. 1105 | if (!$result && $create) 1106 | { 1107 | $this->fields[] = array( 1108 | "id" => false, 1109 | "type" => ($type !== false ? $type : "input.text"), 1110 | "name" => $name, 1111 | "value" => $value, 1112 | "checked" => $checked 1113 | ); 1114 | } 1115 | 1116 | return $result; 1117 | } 1118 | 1119 | public function GenerateFormRequest($submitname = false, $submitvalue = false) 1120 | { 1121 | $method = $this->info["method"]; 1122 | $fields = array(); 1123 | $files = array(); 1124 | foreach ($this->fields as $field) 1125 | { 1126 | if ($field["type"] == "input.file") 1127 | { 1128 | if (is_array($field["value"])) 1129 | { 1130 | $field["value"]["name"] = $field["name"]; 1131 | $files[] = $field["value"]; 1132 | $method = "post"; 1133 | } 1134 | } 1135 | else if ($field["type"] == "input.reset" || $field["type"] == "button.reset") 1136 | { 1137 | } 1138 | else if ($field["type"] == "input.submit" || $field["type"] == "input.image" || $field["type"] == "button.submit") 1139 | { 1140 | if (($submitname === false || $field["name"] === $submitname) && ($submitvalue === false || $field["value"] === $submitvalue)) 1141 | { 1142 | if ($submitname !== "") 1143 | { 1144 | if (!isset($fields[$field["name"]])) $fields[$field["name"]] = array(); 1145 | $fields[$field["name"]][] = $field["value"]; 1146 | } 1147 | 1148 | if ($field["type"] == "input.image") 1149 | { 1150 | if (!isset($fields["x"])) $fields["x"] = array(); 1151 | $fields["x"][] = "1"; 1152 | 1153 | if (!isset($fields["y"])) $fields["y"] = array(); 1154 | $fields["y"][] = "1"; 1155 | } 1156 | } 1157 | } 1158 | else if (($field["type"] != "input.radio" && $field["type"] != "input.checkbox") || $field["checked"]) 1159 | { 1160 | if (!isset($fields[$field["name"]])) $fields[$field["name"]] = array(); 1161 | $fields[$field["name"]][] = $field["value"]; 1162 | } 1163 | } 1164 | 1165 | if ($method == "get") 1166 | { 1167 | $url = HTTP::ExtractURL($this->info["action"]); 1168 | unset($url["query"]); 1169 | $url["queryvars"] = $fields; 1170 | $result = array( 1171 | "url" => HTTP::CondenseURL($url), 1172 | "options" => array() 1173 | ); 1174 | } 1175 | else 1176 | { 1177 | $result = array( 1178 | "url" => $this->info["action"], 1179 | "options" => array( 1180 | "postvars" => $fields, 1181 | "files" => $files 1182 | ) 1183 | ); 1184 | } 1185 | 1186 | return $result; 1187 | } 1188 | } 1189 | ?> -------------------------------------------------------------------------------- /support/http.php: -------------------------------------------------------------------------------- 1 | "", 12 | "authority" => "", 13 | "login" => "", 14 | "loginusername" => "", 15 | "loginpassword" => "", 16 | "host" => "", 17 | "port" => "", 18 | "path" => "", 19 | "query" => "", 20 | "queryvars" => array(), 21 | "fragment" => "" 22 | ); 23 | 24 | $url = str_replace("&", "&", $url); 25 | 26 | $pos = strpos($url, "#"); 27 | if ($pos !== false) 28 | { 29 | $result["fragment"] = substr($url, $pos + 1); 30 | $url = substr($url, 0, $pos); 31 | } 32 | 33 | $pos = strpos($url, "?"); 34 | if ($pos !== false) 35 | { 36 | $result["query"] = str_replace(" ", "+", substr($url, $pos + 1)); 37 | $url = substr($url, 0, $pos); 38 | $vars = explode("&", $result["query"]); 39 | foreach ($vars as $var) 40 | { 41 | $pos = strpos($var, "="); 42 | if ($pos === false) 43 | { 44 | $name = $var; 45 | $value = ""; 46 | } 47 | else 48 | { 49 | $name = substr($var, 0, $pos); 50 | $value = urldecode(substr($var, $pos + 1)); 51 | } 52 | $name = urldecode($name); 53 | if (!isset($result["queryvars"][$name])) $result["queryvars"][$name] = array(); 54 | $result["queryvars"][$name][] = $value; 55 | } 56 | } 57 | 58 | $url = str_replace("\\", "/", $url); 59 | 60 | $pos = strpos($url, ":"); 61 | $pos2 = strpos($url, "/"); 62 | if ($pos !== false && ($pos2 === false || $pos < $pos2)) 63 | { 64 | $result["scheme"] = strtolower(substr($url, 0, $pos)); 65 | $url = substr($url, $pos + 1); 66 | } 67 | 68 | if (substr($url, 0, 2) != "//") $result["path"] = $url; 69 | else 70 | { 71 | $url = substr($url, 2); 72 | $pos = strpos($url, "/"); 73 | if ($pos !== false) 74 | { 75 | $result["path"] = substr($url, $pos); 76 | $url = substr($url, 0, $pos); 77 | } 78 | $result["authority"] = $url; 79 | 80 | $pos = strpos($url, "@"); 81 | if ($pos !== false) 82 | { 83 | $result["login"] = substr($url, 0, $pos); 84 | $url = substr($url, $pos + 1); 85 | $pos = strpos($result["login"], ":"); 86 | if ($pos === false) $result["loginusername"] = urldecode($result["login"]); 87 | else 88 | { 89 | $result["loginusername"] = urldecode(substr($result["login"], 0, $pos)); 90 | $result["loginpassword"] = urldecode(substr($result["login"], $pos + 1)); 91 | } 92 | } 93 | 94 | $pos = strpos($url, "]"); 95 | if (substr($url, 0, 1) == "[" && $pos !== false) 96 | { 97 | // IPv6 literal address. 98 | $result["host"] = substr($url, 0, $pos + 1); 99 | $url = substr($url, $pos + 1); 100 | 101 | $pos = strpos($url, ":"); 102 | if ($pos !== false) 103 | { 104 | $result["port"] = substr($url, $pos + 1); 105 | $url = substr($url, 0, $pos); 106 | } 107 | } 108 | else 109 | { 110 | // Normal host[:port]. 111 | $pos = strpos($url, ":"); 112 | if ($pos !== false) 113 | { 114 | $result["port"] = substr($url, $pos + 1); 115 | $url = substr($url, 0, $pos); 116 | } 117 | 118 | $result["host"] = $url; 119 | 120 | // Handle conversion to Punycode for IDNs. 121 | if ($idnahost) 122 | { 123 | $y = strlen($result["host"]); 124 | for ($x = 0; $x < $y && ord($result["host"][$x]) <= 0x7F; $x++); 125 | 126 | if ($x < $y) 127 | { 128 | if (!class_exists("UTFUtils", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/utf_utils.php"; 129 | 130 | $host = UTFUtils::ConvertToPunycode($result["host"]); 131 | if ($host !== false) 132 | { 133 | $result["orighost"] = $result["host"]; 134 | $result["host"] = $host; 135 | } 136 | } 137 | } 138 | } 139 | } 140 | 141 | return $result; 142 | } 143 | 144 | // Takes a ExtractURL() array and condenses it into a string. 145 | public static function CondenseURL($data) 146 | { 147 | $result = ""; 148 | if (isset($data["host"]) && $data["host"] != "") 149 | { 150 | if (isset($data["scheme"]) && $data["scheme"] != "") $result = $data["scheme"] . "://"; 151 | if (isset($data["loginusername"]) && $data["loginusername"] != "" && isset($data["loginpassword"])) $result .= rawurlencode($data["loginusername"]) . ($data["loginpassword"] != "" ? ":" . rawurlencode($data["loginpassword"]) : "") . "@"; 152 | else if (isset($data["login"]) && $data["login"] != "") $result .= $data["login"] . "@"; 153 | 154 | $result .= $data["host"]; 155 | if (isset($data["port"]) && $data["port"] != "") $result .= ":" . $data["port"]; 156 | 157 | if (isset($data["path"])) 158 | { 159 | $data["path"] = str_replace("\\", "/", $data["path"]); 160 | if (substr($data["path"], 0, 1) != "/") $data["path"] = "/" . $data["path"]; 161 | $result .= $data["path"]; 162 | } 163 | } 164 | else if (isset($data["authority"]) && $data["authority"] != "") 165 | { 166 | if (isset($data["scheme"]) && $data["scheme"] != "") $result = $data["scheme"] . "://"; 167 | 168 | $result .= $data["authority"]; 169 | 170 | if (isset($data["path"])) 171 | { 172 | $data["path"] = str_replace("\\", "/", $data["path"]); 173 | if (substr($data["path"], 0, 1) != "/") $data["path"] = "/" . $data["path"]; 174 | $result .= $data["path"]; 175 | } 176 | } 177 | else if (isset($data["path"])) 178 | { 179 | if (isset($data["scheme"]) && $data["scheme"] != "") $result = $data["scheme"] . ":"; 180 | 181 | $result .= $data["path"]; 182 | } 183 | 184 | if (isset($data["query"])) 185 | { 186 | if ($data["query"] != "") $result .= "?" . $data["query"]; 187 | } 188 | else if (isset($data["queryvars"])) 189 | { 190 | $data["query"] = array(); 191 | foreach ($data["queryvars"] as $key => $vals) 192 | { 193 | if (is_string($vals)) $vals = array($vals); 194 | foreach ($vals as $val) $data["query"][] = urlencode($key) . "=" . urlencode($val); 195 | } 196 | $data["query"] = implode("&", $data["query"]); 197 | 198 | if ($data["query"] != "") $result .= "?" . $data["query"]; 199 | } 200 | 201 | if (isset($data["fragment"]) && $data["fragment"] != "") $result .= "#" . $data["fragment"]; 202 | 203 | return $result; 204 | } 205 | 206 | public static function ConvertRelativeToAbsoluteURL($baseurl, $relativeurl) 207 | { 208 | $relative = (is_array($relativeurl) ? $relativeurl : self::ExtractURL($relativeurl)); 209 | $base = (is_array($baseurl) ? $baseurl : self::ExtractURL($baseurl)); 210 | 211 | if ($relative["host"] != "" || ($relative["scheme"] != "" && $relative["scheme"] != $base["scheme"])) 212 | { 213 | if ($relative["scheme"] == "") $relative["scheme"] = $base["scheme"]; 214 | 215 | return self::CondenseURL($relative); 216 | } 217 | 218 | $result = array( 219 | "scheme" => $base["scheme"], 220 | "loginusername" => $base["loginusername"], 221 | "loginpassword" => $base["loginpassword"], 222 | "host" => $base["host"], 223 | "port" => $base["port"], 224 | "path" => "", 225 | "query" => $relative["query"], 226 | "fragment" => $relative["fragment"] 227 | ); 228 | 229 | if ($relative["path"] == "") $result["path"] = $base["path"]; 230 | else if (substr($relative["path"], 0, 1) == "/") $result["path"] = $relative["path"]; 231 | else 232 | { 233 | $abspath = explode("/", $base["path"]); 234 | array_pop($abspath); 235 | $relpath = explode("/", $relative["path"]); 236 | foreach ($relpath as $piece) 237 | { 238 | if ($piece == ".") 239 | { 240 | } 241 | else if ($piece == "..") array_pop($abspath); 242 | else $abspath[] = $piece; 243 | } 244 | 245 | $abspath = implode("/", $abspath); 246 | if (substr($abspath, 0, 1) != "/") $abspath = "/" . $abspath; 247 | 248 | $result["path"] = $abspath; 249 | } 250 | 251 | return self::CondenseURL($result); 252 | } 253 | 254 | public static function GetUserAgent($type) 255 | { 256 | $type = strtolower($type); 257 | 258 | if ($type == "ie") $type = "ie11"; 259 | 260 | // Last updated March 10, 2022. 261 | 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)"; 262 | else if ($type == "ie7") return "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 6.0)"; 263 | else if ($type == "ie8") return "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; SLCC1)"; 264 | else if ($type == "ie9") return "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)"; 265 | else if ($type == "ie10") return "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)"; 266 | else if ($type == "ie11") return "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko"; 267 | 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"; 268 | else if ($type == "firefox") return "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0"; 269 | 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"; 270 | 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"; 271 | 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"; 272 | 273 | return ""; 274 | } 275 | 276 | public static function GetSSLCiphers($type = "intermediate") 277 | { 278 | $type = strtolower($type); 279 | 280 | // Cipher list last updated March 10, 2022. 281 | // Source: https://ssl-config.mozilla.org/ 282 | 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"; 283 | 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"; 284 | 285 | 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"; 286 | } 287 | 288 | public static function GetSafeSSLOpts($cafile = true, $cipherstype = "intermediate") 289 | { 290 | // Result array last updated Feb 15, 2020. 291 | $result = array( 292 | "ciphers" => self::GetSSLCiphers($cipherstype), 293 | "disable_compression" => true, 294 | "allow_self_signed" => false, 295 | "verify_peer" => true, 296 | "verify_depth" => 5, 297 | "SNI_enabled" => true 298 | ); 299 | 300 | if ($cafile === true) $result["auto_cainfo"] = true; 301 | else if ($cafile !== false) $result["cafile"] = $cafile; 302 | 303 | return $result; 304 | } 305 | 306 | // Reasonably parses RFC1123, RFC850, and asctime() dates. 307 | public static function GetDateTimestamp($httpdate) 308 | { 309 | $timestamp_map = array( 310 | "jan" => 1, "feb" => 2, "mar" => 3, "apr" => 4, "may" => 5, "jun" => 6, 311 | "jul" => 7, "aug" => 8, "sep" => 9, "oct" => 10, "nov" => 11, "dec" => 12 312 | ); 313 | 314 | $year = false; 315 | $month = false; 316 | $day = false; 317 | $hour = false; 318 | $min = false; 319 | $sec = false; 320 | 321 | $items = explode(" ", preg_replace('/\s+/', " ", str_replace("-", " ", strtolower($httpdate)))); 322 | foreach ($items as $item) 323 | { 324 | if ($item != "") 325 | { 326 | if (strpos($item, ":") !== false) 327 | { 328 | $item = explode(":", $item); 329 | $hour = (int)(count($item) > 0 ? array_shift($item) : 0); 330 | $min = (int)(count($item) > 0 ? array_shift($item) : 0); 331 | $sec = (int)(count($item) > 0 ? array_shift($item) : 0); 332 | 333 | if ($hour > 23) $hour = 23; 334 | if ($min > 59) $min = 59; 335 | if ($sec > 59) $sec = 59; 336 | } 337 | else if (is_numeric($item)) 338 | { 339 | if (strlen($item) >= 4) $year = (int)$item; 340 | else if ($day === false) $day = (int)$item; 341 | else $year = substr(date("Y"), 0, 2) . substr($item, -2); 342 | } 343 | else 344 | { 345 | $item = substr($item, 0, 3); 346 | if (isset($timestamp_map[$item])) $month = $timestamp_map[$item]; 347 | } 348 | } 349 | } 350 | 351 | if ($year === false || $month === false || $day === false || $hour === false || $min === false || $sec === false) return false; 352 | 353 | return gmmktime($hour, $min, $sec, $month, $day, $year); 354 | } 355 | 356 | public static function HTTPTranslate() 357 | { 358 | $args = func_get_args(); 359 | if (!count($args)) return ""; 360 | 361 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 362 | } 363 | 364 | public static function HeaderNameCleanup($name) 365 | { 366 | return preg_replace('/\s+/', "-", ucwords(strtolower(trim(preg_replace('/[^A-Za-z0-9 ]/', " ", $name))))); 367 | } 368 | 369 | private static function HeaderValueCleanup($value) 370 | { 371 | return str_replace(array("\r", "\n"), array("", ""), $value); 372 | } 373 | 374 | public static function NormalizeHeaders($headers) 375 | { 376 | $result = array(); 377 | foreach ($headers as $name => $val) 378 | { 379 | $val = self::HeaderValueCleanup($val); 380 | if ($val != "") $result[self::HeaderNameCleanup($name)] = $val; 381 | } 382 | 383 | return $result; 384 | } 385 | 386 | public static function MergeRawHeaders(&$headers, $rawheaders) 387 | { 388 | foreach ($rawheaders as $name => $val) 389 | { 390 | $val = self::HeaderValueCleanup($val); 391 | if ($val != "") 392 | { 393 | $name2 = self::HeaderNameCleanup($name); 394 | if (isset($headers[$name2])) unset($headers[$name2]); 395 | 396 | $headers[$name] = $val; 397 | } 398 | } 399 | } 400 | 401 | public static function ExtractHeader($data) 402 | { 403 | $result = array(); 404 | $data = trim($data); 405 | while ($data != "") 406 | { 407 | // Extract name/value pair. 408 | $pos = strpos($data, "="); 409 | $pos2 = strpos($data, ";"); 410 | if (($pos !== false && $pos2 === false) || ($pos !== false && $pos2 !== false && $pos < $pos2)) 411 | { 412 | $name = trim(substr($data, 0, $pos)); 413 | $data = trim(substr($data, $pos + 1)); 414 | if (ord($data[0]) == ord("\"")) 415 | { 416 | $pos = strpos($data, "\"", 1); 417 | if ($pos !== false) 418 | { 419 | $value = substr($data, 1, $pos - 1); 420 | $data = trim(substr($data, $pos + 1)); 421 | $pos = strpos($data, ";"); 422 | if ($pos !== false) $data = substr($data, $pos + 1); 423 | else $data = ""; 424 | } 425 | else 426 | { 427 | $value = $data; 428 | $data = ""; 429 | } 430 | } 431 | else 432 | { 433 | $pos = strpos($data, ";"); 434 | if ($pos !== false) 435 | { 436 | $value = trim(substr($data, 0, $pos)); 437 | $data = substr($data, $pos + 1); 438 | } 439 | else 440 | { 441 | $value = $data; 442 | $data = ""; 443 | } 444 | } 445 | } 446 | else if ($pos2 !== false) 447 | { 448 | $name = ""; 449 | $value = trim(substr($data, 0, $pos2)); 450 | $data = substr($data, $pos2 + 1); 451 | } 452 | else 453 | { 454 | $name = ""; 455 | $value = $data; 456 | $data = ""; 457 | } 458 | 459 | if ($name != "" || $value != "") $result[strtolower($name)] = $value; 460 | 461 | $data = trim($data); 462 | } 463 | 464 | return $result; 465 | } 466 | 467 | private static function ProcessSSLOptions(&$options, $key, $host) 468 | { 469 | if (isset($options[$key]["auto_cainfo"])) 470 | { 471 | unset($options[$key]["auto_cainfo"]); 472 | 473 | $cainfo = ini_get("curl.cainfo"); 474 | if ($cainfo !== false && strlen($cainfo) > 0) $options[$key]["cafile"] = $cainfo; 475 | else if (file_exists(str_replace("\\", "/", dirname(__FILE__)) . "/cacert.pem")) $options[$key]["cafile"] = str_replace("\\", "/", dirname(__FILE__)) . "/cacert.pem"; 476 | } 477 | 478 | if (isset($options[$key]["auto_peer_name"])) 479 | { 480 | unset($options[$key]["auto_peer_name"]); 481 | 482 | if (!isset($options["headers"]["Host"])) $options[$key]["peer_name"] = $host; 483 | else 484 | { 485 | $info = self::ExtractURL("https://" . $options["headers"]["Host"]); 486 | $options[$key]["peer_name"] = $info["host"]; 487 | } 488 | } 489 | 490 | if (isset($options[$key]["auto_cn_match"])) 491 | { 492 | unset($options[$key]["auto_cn_match"]); 493 | 494 | if (!isset($options["headers"]["Host"])) $options[$key]["CN_match"] = $host; 495 | else 496 | { 497 | $info = self::ExtractURL("https://" . $options["headers"]["Host"]); 498 | $options[$key]["CN_match"] = $info["host"]; 499 | } 500 | } 501 | 502 | if (isset($options[$key]["auto_sni"])) 503 | { 504 | unset($options[$key]["auto_sni"]); 505 | 506 | $options[$key]["SNI_enabled"] = true; 507 | if (!isset($options["headers"]["Host"])) $options[$key]["SNI_server_name"] = $host; 508 | else 509 | { 510 | $info = self::ExtractURL("https://" . $options["headers"]["Host"]); 511 | $options[$key]["SNI_server_name"] = $info["host"]; 512 | } 513 | } 514 | } 515 | 516 | // Swiped from str_basics.php so this file can be standalone. 517 | public static function ExtractFilename($dirfile) 518 | { 519 | $dirfile = str_replace("\\", "/", $dirfile); 520 | $pos = strrpos($dirfile, "/"); 521 | if ($pos !== false) $dirfile = substr($dirfile, $pos + 1); 522 | 523 | return $dirfile; 524 | } 525 | 526 | public static function FilenameSafe($filename) 527 | { 528 | return preg_replace('/[_]+/', "_", preg_replace('/[^A-Za-z0-9_.\-]/', "_", $filename)); 529 | } 530 | 531 | public static function GetTimeLeft($start, $limit) 532 | { 533 | if ($limit === false) return false; 534 | 535 | $difftime = microtime(true) - $start; 536 | if ($difftime >= $limit) return 0; 537 | 538 | return $limit - $difftime; 539 | } 540 | 541 | private static function ProcessRateLimit($size, $start, $limit, $async) 542 | { 543 | $difftime = microtime(true) - $start; 544 | if ($difftime > 0.0) 545 | { 546 | if ($size / $difftime > $limit) 547 | { 548 | // Sleeping for some amount of time will equalize the rate. 549 | // So, solve this for $x: $size / ($x + $difftime) = $limit 550 | $amount = ($size - ($limit * $difftime)) / $limit; 551 | $amount += 0.001; 552 | 553 | if ($async) return microtime(true) + $amount; 554 | else usleep($amount * 1000000); 555 | } 556 | } 557 | 558 | return -1.0; 559 | } 560 | 561 | private static function GetDecodedBody(&$autodecode_ds, $body) 562 | { 563 | if ($autodecode_ds !== false) 564 | { 565 | $autodecode_ds->Write($body); 566 | $body = $autodecode_ds->Read(); 567 | } 568 | 569 | return $body; 570 | } 571 | 572 | private static function StreamTimedOut($fp) 573 | { 574 | if (!function_exists("stream_get_meta_data")) return false; 575 | 576 | $info = stream_get_meta_data($fp); 577 | 578 | return $info["timed_out"]; 579 | } 580 | 581 | public static function InitResponseState($fp, $debug, $options, $startts, $timeout, $result, $close, $nextread, $client = true) 582 | { 583 | $state = array( 584 | "fp" => $fp, 585 | "type" => "response", 586 | "async" => (isset($options["async"]) ? $options["async"] : false), 587 | "debug" => $debug, 588 | "startts" => $startts, 589 | "timeout" => $timeout, 590 | "waituntil" => -1.0, 591 | "rawdata" => "", 592 | "data" => "", 593 | "rawsize" => 0, 594 | "rawrecvheadersize" => 0, 595 | "numheaders" => 0, 596 | "autodecode" => (!isset($options["auto_decode"]) || $options["auto_decode"]), 597 | 598 | "state" => ($client ? "response_line" : "request_line"), 599 | 600 | "options" => $options, 601 | "result" => $result, 602 | "close" => $close, 603 | "nextread" => $nextread, 604 | "client" => $client 605 | ); 606 | 607 | $state["result"]["recvstart"] = microtime(true); 608 | $state["result"]["response"] = false; 609 | $state["result"]["headers"] = false; 610 | $state["result"]["body"] = false; 611 | 612 | return $state; 613 | } 614 | 615 | // Handles partially read input. Also deals with the hacky workaround to the second bugfix in ProcessState__WriteData(). 616 | private static function ProcessState__InternalRead(&$state, $size, $endchar = false) 617 | { 618 | $y = strlen($state["nextread"]); 619 | 620 | do 621 | { 622 | if ($size <= $y) 623 | { 624 | if ($endchar === false) $pos = $size; 625 | else 626 | { 627 | $pos = strpos($state["nextread"], $endchar); 628 | if ($pos === false || $pos > $size) $pos = $size; 629 | else $pos++; 630 | } 631 | 632 | $data = substr($state["nextread"], 0, $pos); 633 | $state["nextread"] = (string)substr($state["nextread"], $pos); 634 | 635 | return $data; 636 | } 637 | 638 | if ($endchar !== false) 639 | { 640 | $pos = strpos($state["nextread"], $endchar); 641 | if ($pos !== false) 642 | { 643 | $data = substr($state["nextread"], 0, $pos + 1); 644 | $state["nextread"] = (string)substr($state["nextread"], $pos + 1); 645 | 646 | return $data; 647 | } 648 | } 649 | 650 | if ($state["debug"]) $data2 = fread($state["fp"], $size); 651 | else $data2 = @fread($state["fp"], $size); 652 | 653 | if ($data2 === false || $data2 === "") 654 | { 655 | if ($state["nextread"] === "") return $data2; 656 | 657 | if ($state["async"] && $endchar !== false && $data2 === "") return ""; 658 | 659 | $data = $state["nextread"]; 660 | $state["nextread"] = ""; 661 | 662 | return $data; 663 | } 664 | 665 | $state["nextread"] .= $data2; 666 | 667 | $y = strlen($state["nextread"]); 668 | } while (!$state["async"] || ($size <= $y) || ($endchar !== false && strpos($state["nextread"], $endchar) !== false)); 669 | 670 | if ($endchar !== false) return ""; 671 | 672 | $data = $state["nextread"]; 673 | $state["nextread"] = ""; 674 | 675 | return $data; 676 | } 677 | 678 | // Reads one line. 679 | private static function ProcessState__ReadLine(&$state) 680 | { 681 | while (strpos($state["data"], "\n") === false) 682 | { 683 | $data2 = self::ProcessState__InternalRead($state, 116000, "\n"); 684 | 685 | if ($data2 === false || $data2 === "") 686 | { 687 | if (feof($state["fp"])) return array("success" => false, "error" => self::HTTPTranslate("Remote peer disconnected."), "errorcode" => "peer_disconnected"); 688 | else if ($state["async"]) return array("success" => false, "error" => self::HTTPTranslate("Non-blocking read returned no data."), "errorcode" => "no_data"); 689 | else if ($data2 === false) return array("success" => false, "error" => self::HTTPTranslate("Underlying stream encountered a read error."), "errorcode" => "stream_read_error"); 690 | } 691 | $pos = strpos($data2, "\n"); 692 | if ($pos === false) 693 | { 694 | if (feof($state["fp"])) return array("success" => false, "error" => self::HTTPTranslate("Remote peer disconnected."), "errorcode" => "peer_disconnected"); 695 | if (self::StreamTimedOut($state["fp"])) return array("success" => false, "error" => self::HTTPTranslate("Underlying stream timed out."), "errorcode" => "stream_timeout_exceeded"); 696 | 697 | $pos = strlen($data2); 698 | } 699 | if ($state["timeout"] !== false && self::GetTimeLeft($state["startts"], $state["timeout"]) == 0) return array("success" => false, "error" => self::HTTPTranslate("HTTP timeout exceeded."), "errorcode" => "timeout_exceeded"); 700 | 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"); 701 | 702 | $state["rawsize"] += strlen($data2); 703 | $state["data"] .= $data2; 704 | 705 | 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"); 706 | if (isset($state["options"]["recvratelimit"])) $state["waituntil"] = self::ProcessRateLimit($state["rawsize"], $state["recvstart"], $state["options"]["recvratelimit"], $state["async"]); 707 | 708 | 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"])); 709 | else if ($state["debug"]) $state["rawdata"] .= $data2; 710 | } 711 | 712 | return array("success" => true); 713 | } 714 | 715 | // Reads data in. 716 | private static function ProcessState__ReadBodyData(&$state) 717 | { 718 | while ($state["sizeleft"] === false || $state["sizeleft"] > 0) 719 | { 720 | $data2 = self::ProcessState__InternalRead($state, ($state["sizeleft"] === false || $state["sizeleft"] > 65536 ? 65536 : $state["sizeleft"])); 721 | 722 | if ($data2 === false) return array("success" => false, "error" => self::HTTPTranslate("Underlying stream encountered a read error."), "errorcode" => "stream_read_error"); 723 | if ($data2 === "") 724 | { 725 | if (feof($state["fp"])) return array("success" => false, "error" => self::HTTPTranslate("Remote peer disconnected."), "errorcode" => "peer_disconnected"); 726 | if (self::StreamTimedOut($state["fp"])) return array("success" => false, "error" => self::HTTPTranslate("Underlying stream timed out."), "errorcode" => "stream_timeout_exceeded"); 727 | 728 | if ($state["async"]) return array("success" => false, "error" => self::HTTPTranslate("Non-blocking read returned no data."), "errorcode" => "no_data"); 729 | } 730 | if ($state["timeout"] !== false && self::GetTimeLeft($state["startts"], $state["timeout"]) == 0) return array("success" => false, "error" => self::HTTPTranslate("HTTP timeout exceeded."), "errorcode" => "timeout_exceeded"); 731 | 732 | $tempsize = strlen($data2); 733 | $state["rawsize"] += $tempsize; 734 | if ($state["sizeleft"] !== false) $state["sizeleft"] -= $tempsize; 735 | 736 | 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); 737 | 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"); 738 | 739 | 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"); 740 | 741 | if (isset($state["options"]["recvratelimit"])) 742 | { 743 | $state["waituntil"] = self::ProcessRateLimit($state["rawsize"], $state["recvstart"], $state["options"]["recvratelimit"], $state["async"]); 744 | 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"); 745 | } 746 | 747 | 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"])); 748 | else if ($state["debug"]) $state["rawdata"] .= $data2; 749 | } 750 | 751 | return array("success" => true); 752 | } 753 | 754 | // Writes data out. 755 | private static function ProcessState__WriteData(&$state, $prefix) 756 | { 757 | if ($state[$prefix . "data"] !== "") 758 | { 759 | // Serious bug in PHP core for non-blocking SSL sockets: https://bugs.php.net/bug.php?id=72333 760 | if ($state["secure"] && $state["async"] && version_compare(PHP_VERSION, "7.1.4") <= 0) 761 | { 762 | // This is a huge hack that has a pretty good chance of blocking on the socket. 763 | // Peeling off up to just 4KB at a time helps to minimize that possibility. It's better than guaranteed failure of the socket though. 764 | @stream_set_blocking($state["fp"], 1); 765 | if ($state["debug"]) $result = fwrite($state["fp"], (strlen($state[$prefix . "data"]) > 4096 ? substr($state[$prefix . "data"], 0, 4096) : $state[$prefix . "data"])); 766 | else $result = @fwrite($state["fp"], (strlen($state[$prefix . "data"]) > 4096 ? substr($state[$prefix . "data"], 0, 4096) : $state[$prefix . "data"])); 767 | @stream_set_blocking($state["fp"], 0); 768 | } 769 | else 770 | { 771 | if ($state["debug"]) $result = fwrite($state["fp"], $state[$prefix . "data"]); 772 | else $result = @fwrite($state["fp"], $state[$prefix . "data"]); 773 | } 774 | 775 | 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"); 776 | 777 | // Serious bug in PHP core for all socket types: https://bugs.php.net/bug.php?id=73535 778 | if ($result === 0) 779 | { 780 | // Temporarily switch to non-blocking sockets and test a one byte read (doesn't matter if data is available or not). 781 | // This is a bigger hack than the first hack above. 782 | if (!$state["async"]) @stream_set_blocking($state["fp"], 0); 783 | 784 | if ($state["debug"]) $data2 = fread($state["fp"], 1); 785 | else $data2 = @fread($state["fp"], 1); 786 | 787 | if ($data2 === false) return array("success" => false, "error" => self::HTTPTranslate("Underlying stream encountered a read error."), "errorcode" => "stream_read_error"); 788 | if ($data2 === "" && feof($state["fp"])) return array("success" => false, "error" => self::HTTPTranslate("Remote peer disconnected."), "errorcode" => "peer_disconnected"); 789 | 790 | if ($data2 !== "") $state["nextread"] .= $data2; 791 | 792 | if (!$state["async"]) @stream_set_blocking($state["fp"], 1); 793 | } 794 | 795 | if ($state["timeout"] !== false && self::GetTimeLeft($state["startts"], $state["timeout"]) == 0) return array("success" => false, "error" => self::HTTPTranslate("HTTP timeout exceeded."), "errorcode" => "timeout_exceeded"); 796 | 797 | $data2 = (string)substr($state[$prefix . "data"], 0, $result); 798 | $state[$prefix . "data"] = (string)substr($state[$prefix . "data"], $result); 799 | 800 | $state["result"]["rawsend" . $prefix . "size"] += $result; 801 | 802 | if (isset($state["options"]["sendratelimit"])) 803 | { 804 | $state["waituntil"] = self::ProcessRateLimit($state["result"]["rawsendsize"], $state["result"]["connected"], $state["options"]["sendratelimit"], $state["async"]); 805 | 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"); 806 | } 807 | 808 | 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"])); 809 | else if ($state["debug"]) $state["result"]["rawsend"] .= $data2; 810 | 811 | 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"); 812 | } 813 | 814 | return array("success" => true); 815 | } 816 | 817 | public static function ForceClose(&$state) 818 | { 819 | if ($state["fp"] !== false) 820 | { 821 | @fclose($state["fp"]); 822 | $state["fp"] = false; 823 | } 824 | 825 | if (isset($state["currentfile"]) && $state["currentfile"] !== false) 826 | { 827 | if ($state["currentfile"]["fp"] !== false) @fclose($state["currentfile"]["fp"]); 828 | $state["currentfile"] = false; 829 | } 830 | } 831 | 832 | private static function CleanupErrorState(&$state, $result) 833 | { 834 | if (!$result["success"] && $result["errorcode"] !== "no_data") 835 | { 836 | self::ForceClose($state); 837 | 838 | $state["error"] = $result; 839 | } 840 | 841 | return $result; 842 | } 843 | 844 | public static function WantRead(&$state) 845 | { 846 | return ($state["type"] === "response" || $state["state"] === "proxy_connect_response" || $state["state"] === "receive_switch" || $state["state"] === "connecting_enable_crypto" || $state["state"] === "proxy_connect_enable_crypto"); 847 | } 848 | 849 | public static function WantWrite(&$state) 850 | { 851 | return (!self::WantRead($state) || $state["state"] === "connecting_enable_crypto" || $state["state"] === "proxy_connect_enable_crypto"); 852 | } 853 | 854 | public static function ProcessState(&$state) 855 | { 856 | if (isset($state["error"])) return $state["error"]; 857 | 858 | 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")); 859 | 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"); 860 | 861 | if ($state["type"] === "request") 862 | { 863 | while ($state["state"] !== "done") 864 | { 865 | switch ($state["state"]) 866 | { 867 | case "connecting": 868 | { 869 | if (function_exists("stream_select") && $state["async"]) 870 | { 871 | $readfp = NULL; 872 | $writefp = array($state["fp"]); 873 | $exceptfp = array($state["fp"]); 874 | if ($state["debug"]) $result = stream_select($readfp, $writefp, $exceptfp, 0); 875 | else $result = @stream_select($readfp, $writefp, $exceptfp, 0); 876 | 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")); 877 | 878 | if (!count($writefp)) return array("success" => false, "error" => self::HTTPTranslate("Connection not established yet."), "errorcode" => "no_data"); 879 | } 880 | 881 | // Deal with failed connections that hang applications. 882 | if (isset($state["options"]["streamtimeout"]) && $state["options"]["streamtimeout"] !== false && function_exists("stream_set_timeout")) @stream_set_timeout($state["fp"], $state["options"]["streamtimeout"]); 883 | 884 | // Switch to the next state. 885 | if ($state["async"] && function_exists("stream_socket_client") && (($state["useproxy"] && $state["proxysecure"]) || (!$state["useproxy"] && $state["secure"]))) $state["state"] = "connecting_enable_crypto"; 886 | else $state["state"] = "connection_ready"; 887 | 888 | 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"])); 889 | 890 | break; 891 | } 892 | case "connecting_enable_crypto": 893 | { 894 | // This is only used by clients that connect asynchronously via SSL. 895 | if ($state["debug"]) $result = stream_socket_enable_crypto($state["fp"], true, STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT); 896 | else $result = @stream_socket_enable_crypto($state["fp"], true, STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT); 897 | 898 | 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")); 899 | else if ($result === true) $state["state"] = "connection_ready"; 900 | else return array("success" => false, "error" => self::HTTPTranslate("Non-blocking enable crypto operation is not complete yet."), "errorcode" => "no_data"); 901 | 902 | 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"])); 903 | 904 | break; 905 | } 906 | case "connection_ready": 907 | { 908 | // Handle peer certificate retrieval. 909 | if (function_exists("stream_context_get_options")) 910 | { 911 | $contextopts = stream_context_get_options($state["fp"]); 912 | if ($state["useproxy"]) 913 | { 914 | if ($state["proxysecure"] && isset($state["options"]["proxysslopts"]) && is_array($state["options"]["proxysslopts"])) 915 | { 916 | if (isset($state["options"]["peer_cert_callback"]) && is_callable($state["options"]["peer_cert_callback"])) 917 | { 918 | 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"); 919 | 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"); 920 | } 921 | } 922 | } 923 | else 924 | { 925 | if ($state["secure"] && isset($state["options"]["sslopts"]) && is_array($state["options"]["sslopts"])) 926 | { 927 | if (isset($state["options"]["peer_cert_callback"]) && is_callable($state["options"]["peer_cert_callback"])) 928 | { 929 | 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"); 930 | 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"); 931 | } 932 | } 933 | } 934 | } 935 | 936 | $state["result"]["connected"] = microtime(true); 937 | 938 | // Switch to the correct state. 939 | if ($state["proxyconnect"]) 940 | { 941 | $state["result"]["rawsendproxysize"] = 0; 942 | $state["result"]["rawsendproxyheadersize"] = strlen($state["proxydata"]); 943 | 944 | $state["state"] = "proxy_connect_send"; 945 | } 946 | else 947 | { 948 | $state["result"]["sendstart"] = microtime(true); 949 | 950 | $state["state"] = "send_data"; 951 | } 952 | 953 | 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"])); 954 | 955 | break; 956 | } 957 | case "proxy_connect_send": 958 | { 959 | // Send the HTTP CONNECT request to the proxy. 960 | $result = self::ProcessState__WriteData($state, "proxy"); 961 | if (!$result["success"]) return self::CleanupErrorState($state, $result); 962 | 963 | // Prepare the state for handling the response from the proxy server. 964 | $options2 = array(); 965 | if (isset($state["options"]["async"])) $options2["async"] = $state["options"]["async"]; 966 | if (isset($state["options"]["recvratelimit"])) $options2["recvratelimit"] = $state["options"]["recvratelimit"]; 967 | if (isset($state["options"]["debug_callback"])) 968 | { 969 | $options2["debug_callback"] = $state["options"]["debug_callback"]; 970 | $options2["debug_callback_opts"] = $state["options"]["debug_callback_opts"]; 971 | } 972 | $state["proxyresponse"] = self::InitResponseState($state["fp"], $state["debug"], $options2, $state["startts"], $state["timeout"], $state["result"], false, $state["nextread"]); 973 | $state["proxyresponse"]["proxyconnect"] = true; 974 | 975 | $state["state"] = "proxy_connect_response"; 976 | 977 | 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"])); 978 | 979 | break; 980 | } 981 | case "proxy_connect_response": 982 | { 983 | // Recursively call this function to handle the proxy response. 984 | $result = self::ProcessState($state["proxyresponse"]); 985 | if (!$result["success"]) return self::CleanupErrorState($state, $result); 986 | 987 | $state["result"]["rawrecvsize"] += $result["rawrecvsize"]; 988 | $state["result"]["rawrecvheadersize"] += $result["rawrecvheadersize"]; 989 | 990 | 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")); 991 | 992 | // Proxy connect tunnel established. Proceed normally. 993 | $state["result"]["sendstart"] = microtime(true); 994 | 995 | if ($state["secure"]) $state["state"] = "proxy_connect_enable_crypto"; 996 | else $state["state"] = "send_data"; 997 | 998 | 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"])); 999 | 1000 | break; 1001 | } 1002 | case "proxy_connect_enable_crypto": 1003 | { 1004 | if ($state["debug"]) $result = stream_socket_enable_crypto($state["fp"], true, STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT); 1005 | else $result = @stream_socket_enable_crypto($state["fp"], true, STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT); 1006 | 1007 | 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")); 1008 | else if ($result === true) 1009 | { 1010 | // Handle peer certificate retrieval. 1011 | if (function_exists("stream_context_get_options")) 1012 | { 1013 | $contextopts = stream_context_get_options($state["fp"]); 1014 | 1015 | if (isset($state["options"]["sslopts"]) && is_array($state["options"]["sslopts"])) 1016 | { 1017 | if (isset($state["options"]["peer_cert_callback"]) && is_callable($state["options"]["peer_cert_callback"])) 1018 | { 1019 | 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"); 1020 | 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"); 1021 | } 1022 | } 1023 | } 1024 | 1025 | // Secure connection established. 1026 | $state["state"] = "send_data"; 1027 | 1028 | 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"])); 1029 | } 1030 | 1031 | break; 1032 | } 1033 | case "send_data": 1034 | { 1035 | // Send the queued data. 1036 | $result = self::ProcessState__WriteData($state, ""); 1037 | if (!$result["success"]) return self::CleanupErrorState($state, $result); 1038 | 1039 | // Queue up more data. 1040 | if (isset($state["options"]["write_body_callback"]) && is_callable($state["options"]["write_body_callback"])) 1041 | { 1042 | if ($state["bodysize"] === false || $state["bodysize"] > 0) 1043 | { 1044 | $bodysize2 = $state["bodysize"]; 1045 | $result = call_user_func_array($state["options"]["write_body_callback"], array(&$state["data"], &$bodysize2, &$state["options"]["write_body_callback_opts"])); 1046 | 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")); 1047 | 1048 | if ($state["bodysize"] === false) 1049 | { 1050 | if ($state["data"] !== "" && $state["chunked"]) $state["data"] = dechex(strlen($state["data"])) . "\r\n" . $state["data"] . "\r\n"; 1051 | 1052 | // When $bodysize2 is set to true, it is the last chunk. 1053 | if ($bodysize2 === true) 1054 | { 1055 | if ($state["chunked"]) 1056 | { 1057 | $state["data"] .= "0\r\n"; 1058 | 1059 | // Allow the body callback function to append additional headers to the content to send. 1060 | // It is up to the callback function to correctly format the extra headers. 1061 | $result = call_user_func_array($state["options"]["write_body_callback"], array(&$state["data"], &$bodysize2, &$state["options"]["write_body_callback_opts"])); 1062 | if (!$result) return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("HTTP write body callback function failed."), "errorcode" => "write_body_callback")); 1063 | 1064 | $state["data"] .= "\r\n"; 1065 | } 1066 | 1067 | $state["bodysize"] = 0; 1068 | } 1069 | } 1070 | else 1071 | { 1072 | $state["bodysize"] -= strlen($state["data"]); 1073 | } 1074 | } 1075 | } 1076 | else if (isset($state["options"]["files"]) && $state["bodysize"] > 0) 1077 | { 1078 | // Select the next file to upload. 1079 | if ($state["currentfile"] === false && count($state["options"]["files"])) 1080 | { 1081 | $state["currentfile"] = array_shift($state["options"]["files"]); 1082 | 1083 | $name = self::HeaderValueCleanup($state["currentfile"]["name"]); 1084 | $name = str_replace("\"", "", $name); 1085 | $filename = self::FilenameSafe(self::ExtractFilename($state["currentfile"]["filename"])); 1086 | $type = self::HeaderValueCleanup($state["currentfile"]["type"]); 1087 | 1088 | $state["data"] = "--" . $state["mime"] . "\r\n"; 1089 | $state["data"] .= "Content-Disposition: form-data; name=\"" . $name . "\"; filename=\"" . $filename . "\"\r\n"; 1090 | $state["data"] .= "Content-Type: " . $type . "\r\n"; 1091 | $state["data"] .= "\r\n"; 1092 | 1093 | if (!isset($state["currentfile"]["datafile"])) 1094 | { 1095 | $state["data"] .= $state["currentfile"]["data"]; 1096 | $state["data"] .= "\r\n"; 1097 | 1098 | $state["currentfile"] = false; 1099 | } 1100 | else 1101 | { 1102 | $state["currentfile"]["fp"] = @fopen($state["currentfile"]["datafile"], "rb"); 1103 | 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")); 1104 | } 1105 | } 1106 | 1107 | // Process the next chunk of file information. 1108 | if ($state["currentfile"] !== false && isset($state["currentfile"]["fp"])) 1109 | { 1110 | // Read/Write up to 65K at a time. 1111 | if ($state["currentfile"]["filesize"] >= 65536) 1112 | { 1113 | $data2 = fread($state["currentfile"]["fp"], 65536); 1114 | 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")); 1115 | 1116 | $state["data"] .= $data2; 1117 | 1118 | $state["currentfile"]["filesize"] -= 65536; 1119 | } 1120 | else 1121 | { 1122 | // Read in the rest. 1123 | if ($state["currentfile"]["filesize"] > 0) 1124 | { 1125 | $data2 = fread($state["currentfile"]["fp"], $state["currentfile"]["filesize"]); 1126 | 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")); 1127 | 1128 | $state["data"] .= $data2; 1129 | } 1130 | 1131 | $state["data"] .= "\r\n"; 1132 | 1133 | fclose($state["currentfile"]["fp"]); 1134 | 1135 | $state["currentfile"] = false; 1136 | } 1137 | } 1138 | 1139 | // If there is no more data, write out the closing MIME line. 1140 | if ($state["data"] === "") $state["data"] = "--" . $state["mime"] . "--\r\n"; 1141 | 1142 | $state["bodysize"] -= strlen($state["data"]); 1143 | } 1144 | else if ($state["bodysize"] === false || $state["bodysize"] > 0) 1145 | { 1146 | return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("A weird internal HTTP error that should never, ever happen...just happened."), "errorcode" => "impossible")); 1147 | } 1148 | 1149 | // All done sending data. 1150 | if ($state["data"] === "") 1151 | { 1152 | if ($state["client"]) 1153 | { 1154 | $state["state"] = "receive_switch"; 1155 | 1156 | 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"])); 1157 | } 1158 | else 1159 | { 1160 | $state["result"]["endts"] = microtime(true); 1161 | 1162 | if ($state["close"]) fclose($state["fp"]); 1163 | else $state["result"]["fp"] = $state["fp"]; 1164 | 1165 | return $state["result"]; 1166 | } 1167 | } 1168 | 1169 | break; 1170 | } 1171 | case "receive_switch": 1172 | { 1173 | if (function_exists("stream_select") && $state["async"]) 1174 | { 1175 | $readfp = array($state["fp"]); 1176 | $writefp = NULL; 1177 | $exceptfp = NULL; 1178 | if ($state["debug"]) $result = stream_select($readfp, $writefp, $exceptfp, 0); 1179 | else $result = @stream_select($readfp, $writefp, $exceptfp, 0); 1180 | 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")); 1181 | 1182 | if (!count($readfp)) return array("success" => false, "error" => self::HTTPTranslate("Connection not fully established yet."), "errorcode" => "no_data"); 1183 | } 1184 | 1185 | $state["state"] = "done"; 1186 | 1187 | 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"])); 1188 | 1189 | break; 1190 | } 1191 | } 1192 | } 1193 | 1194 | // The request has been sent. Change the state to a response state. 1195 | $state = self::InitResponseState($state["fp"], $state["debug"], $state["options"], $state["startts"], $state["timeout"], $state["result"], $state["close"], $state["nextread"]); 1196 | 1197 | // Run one cycle. 1198 | return self::ProcessState($state); 1199 | } 1200 | else if ($state["type"] === "response") 1201 | { 1202 | while ($state["state"] !== "done") 1203 | { 1204 | switch ($state["state"]) 1205 | { 1206 | case "response_line": 1207 | { 1208 | $result = self::ProcessState__ReadLine($state); 1209 | if (!$result["success"]) return self::CleanupErrorState($state, $result); 1210 | 1211 | // Parse the response line. 1212 | $pos = strpos($state["data"], "\n"); 1213 | if ($pos === false) return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("Unable to retrieve response line."), "errorcode" => "get_response_line")); 1214 | $line = trim(substr($state["data"], 0, $pos)); 1215 | $state["data"] = substr($state["data"], $pos + 1); 1216 | $state["rawrecvheadersize"] += $pos + 1; 1217 | $response = explode(" ", $line, 3); 1218 | 1219 | $state["result"]["response"] = array( 1220 | "line" => $line, 1221 | "httpver" => strtoupper($response[0]), 1222 | "code" => $response[1], 1223 | "meaning" => (isset($response[2]) ? $response[2] : "") 1224 | ); 1225 | 1226 | $state["state"] = "headers"; 1227 | $state["result"]["headers"] = array(); 1228 | $state["lastheader"] = ""; 1229 | 1230 | 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"])); 1231 | 1232 | break; 1233 | } 1234 | case "request_line": 1235 | { 1236 | // Server mode only. 1237 | $result = self::ProcessState__ReadLine($state); 1238 | if (!$result["success"]) return self::CleanupErrorState($state, $result); 1239 | 1240 | // Parse the request line. 1241 | $pos = strpos($state["data"], "\n"); 1242 | if ($pos === false) return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("Unable to retrieve request line."), "errorcode" => "get_request_line")); 1243 | $line = trim(substr($state["data"], 0, $pos)); 1244 | $state["data"] = substr($state["data"], $pos + 1); 1245 | $state["rawrecvheadersize"] += $pos + 1; 1246 | 1247 | $request = $line; 1248 | $pos = strpos($request, " "); 1249 | if ($pos === false) $pos = strlen($request); 1250 | $method = (string)substr($request, 0, $pos); 1251 | $request = trim(substr($request, $pos)); 1252 | 1253 | $pos = strrpos($request, " "); 1254 | if ($pos === false) $pos = strlen($request); 1255 | $path = trim(substr($request, 0, $pos)); 1256 | if ($path === "") $path = "/"; 1257 | $version = (string)substr($request, $pos + 1); 1258 | 1259 | $state["result"]["request"] = array( 1260 | "line" => $line, 1261 | "method" => strtoupper($method), 1262 | "path" => $path, 1263 | "httpver" => strtoupper($version), 1264 | ); 1265 | 1266 | // Fake the response line to bypass some client-only code. 1267 | $state["result"]["response"] = array( 1268 | "line" => "200", 1269 | "httpver" => "", 1270 | "code" => 200, 1271 | "meaning" => "" 1272 | ); 1273 | 1274 | $state["state"] = "headers"; 1275 | $state["result"]["headers"] = array(); 1276 | $state["lastheader"] = ""; 1277 | 1278 | 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"])); 1279 | 1280 | break; 1281 | } 1282 | case "headers": 1283 | case "body_chunked_headers": 1284 | { 1285 | $result = self::ProcessState__ReadLine($state); 1286 | if (!$result["success"] && ($state["state"] === "headers" || ($result["errorcode"] !== "stream_read_error" && $result["errorcode"] !== "peer_disconnected"))) return self::CleanupErrorState($state, $result); 1287 | 1288 | $pos = strpos($state["data"], "\n"); 1289 | if ($pos === false) $pos = strlen($state["data"]); 1290 | $header = rtrim(substr($state["data"], 0, $pos)); 1291 | $state["data"] = substr($state["data"], $pos + 1); 1292 | $state["rawrecvheadersize"] += $pos + 1; 1293 | if ($header != "") 1294 | { 1295 | 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; 1296 | else 1297 | { 1298 | $pos = strpos($header, ":"); 1299 | if ($pos === false) $pos = strlen($header); 1300 | $state["lastheader"] = self::HeaderNameCleanup(substr($header, 0, $pos)); 1301 | if (!isset($state["result"]["headers"][$state["lastheader"]])) $state["result"]["headers"][$state["lastheader"]] = array(); 1302 | $state["result"]["headers"][$state["lastheader"]][] = ltrim(substr($header, $pos + 1)); 1303 | } 1304 | 1305 | $state["numheaders"]++; 1306 | 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")); 1307 | } 1308 | else 1309 | { 1310 | if ($state["result"]["response"]["code"] != 100 && isset($state["options"]["read_headers_callback"]) && is_callable($state["options"]["read_headers_callback"])) 1311 | { 1312 | 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")); 1313 | } 1314 | 1315 | // Additional headers (optional) are the last bit of data in a chunked response. 1316 | if ($state["state"] === "body_chunked_headers") 1317 | { 1318 | $state["state"] = "body_finalize"; 1319 | 1320 | 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"])); 1321 | } 1322 | else 1323 | { 1324 | $state["result"]["body"] = ""; 1325 | 1326 | // Handle 100 Continue below OR WebSocket among other things by letting the caller handle reading the body. 1327 | if ($state["result"]["response"]["code"] == 100 || $state["result"]["response"]["code"] == 101) 1328 | { 1329 | $state["state"] = "done"; 1330 | 1331 | 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"])); 1332 | } 1333 | else 1334 | { 1335 | // Determine if decoding the content is possible and necessary. 1336 | 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; 1337 | if (!$state["autodecode"]) $state["autodecode_ds"] = false; 1338 | else 1339 | { 1340 | if (!class_exists("DeflateStream", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/deflate_stream.php"; 1341 | 1342 | // Since servers and browsers do everything wrong, ignore the encoding claim and attempt to auto-detect the encoding. 1343 | $state["autodecode_ds"] = new DeflateStream(); 1344 | $state["autodecode_ds"]->Init("rb", -1, array("type" => "auto")); 1345 | } 1346 | 1347 | // Use the appropriate state for handling the next bit of input. 1348 | if (isset($state["result"]["headers"]["Transfer-Encoding"]) && strtolower($state["result"]["headers"]["Transfer-Encoding"][0]) == "chunked") 1349 | { 1350 | $state["state"] = "body_chunked_size"; 1351 | } 1352 | else 1353 | { 1354 | $state["sizeleft"] = (isset($state["result"]["headers"]["Content-Length"]) ? (double)preg_replace('/[^0-9]/', "", $state["result"]["headers"]["Content-Length"][0]) : false); 1355 | $state["state"] = (!isset($state["proxyconnect"]) && ($state["sizeleft"] !== false || $state["client"]) ? "body_content" : "done"); 1356 | } 1357 | 1358 | 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"])); 1359 | 1360 | // Let servers have a chance to alter limits before processing the input body. 1361 | if (!$state["client"] && $state["state"] !== "done") return array("success" => false, "error" => self::HTTPTranslate("Intermission for adjustments to limits."), "errorcode" => "no_data"); 1362 | } 1363 | } 1364 | } 1365 | 1366 | break; 1367 | } 1368 | case "body_chunked_size": 1369 | { 1370 | $result = self::ProcessState__ReadLine($state); 1371 | if (!$result["success"]) return self::CleanupErrorState($state, $result); 1372 | 1373 | $pos = strpos($state["data"], "\n"); 1374 | if ($pos === false) $pos = strlen($state["data"]); 1375 | $line = trim(substr($state["data"], 0, $pos)); 1376 | $state["data"] = substr($state["data"], $pos + 1); 1377 | $pos = strpos($line, ";"); 1378 | if ($pos === false) $pos = strlen($line); 1379 | $size = hexdec(substr($line, 0, $pos)); 1380 | if ($size < 0) $size = 0; 1381 | 1382 | // Retrieve content. 1383 | $size2 = $size; 1384 | $size3 = min(strlen($state["data"]), $size); 1385 | if ($size3 > 0) 1386 | { 1387 | $data2 = substr($state["data"], 0, $size3); 1388 | $state["data"] = substr($state["data"], $size3); 1389 | $size2 -= $size3; 1390 | 1391 | 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); 1392 | 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")); 1393 | } 1394 | 1395 | $state["chunksize"] = $size; 1396 | $state["sizeleft"] = $size2; 1397 | $state["state"] = "body_chunked_data"; 1398 | 1399 | 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"])); 1400 | 1401 | break; 1402 | } 1403 | case "body_chunked_data": 1404 | { 1405 | $result = self::ProcessState__ReadBodyData($state); 1406 | if (!$result["success"]) return self::CleanupErrorState($state, $result); 1407 | 1408 | if ($state["chunksize"] > 0) $state["state"] = "body_chunked_skipline"; 1409 | else 1410 | { 1411 | $state["lastheader"] = ""; 1412 | $state["state"] = "body_chunked_headers"; 1413 | } 1414 | 1415 | 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"])); 1416 | 1417 | break; 1418 | } 1419 | case "body_chunked_skipline": 1420 | { 1421 | $result = self::ProcessState__ReadLine($state); 1422 | if (!$result["success"]) return self::CleanupErrorState($state, $result); 1423 | 1424 | // Ignore one newline. 1425 | $pos = strpos($state["data"], "\n"); 1426 | if ($pos === false) $pos = strlen($state["data"]); 1427 | $state["data"] = substr($state["data"], $pos + 1); 1428 | 1429 | $state["state"] = "body_chunked_size"; 1430 | 1431 | 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"])); 1432 | 1433 | break; 1434 | } 1435 | case "body_content": 1436 | { 1437 | $result = self::ProcessState__ReadBodyData($state); 1438 | 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); 1439 | 1440 | $state["state"] = "body_finalize"; 1441 | 1442 | 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"])); 1443 | 1444 | break; 1445 | } 1446 | case "body_finalize": 1447 | { 1448 | if ($state["autodecode_ds"] !== false) 1449 | { 1450 | $state["autodecode_ds"]->Finalize(); 1451 | $data2 = $state["autodecode_ds"]->Read(); 1452 | 1453 | if ($state["result"]["response"]["code"] == 100 || !isset($state["options"]["read_body_callback"]) || !is_callable($state["options"]["read_body_callback"])) $state["result"]["body"] .= $data2; 1454 | 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")); 1455 | } 1456 | 1457 | $state["state"] = "done"; 1458 | 1459 | 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"])); 1460 | 1461 | break; 1462 | } 1463 | } 1464 | 1465 | // Handle HTTP 100 Continue status codes. 1466 | if ($state["state"] === "done" && $state["result"]["response"]["code"] == 100) 1467 | { 1468 | $state["autodecode"] = (!isset($state["options"]["auto_decode"]) || $state["options"]["auto_decode"]); 1469 | $state["state"] = "response"; 1470 | $state["result"]["response"] = false; 1471 | $state["result"]["headers"] = false; 1472 | $state["result"]["body"] = false; 1473 | 1474 | 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"])); 1475 | } 1476 | } 1477 | 1478 | if ($state["debug"]) $state["result"]["rawrecv"] .= $state["rawdata"]; 1479 | $state["result"]["rawrecvsize"] += $state["rawsize"]; 1480 | $state["result"]["rawrecvheadersize"] += $state["rawrecvheadersize"]; 1481 | $state["result"]["endts"] = microtime(true); 1482 | 1483 | if ($state["close"] || ($state["client"] && isset($state["result"]["headers"]["Connection"]) && strtolower($state["result"]["headers"]["Connection"][0]) === "close")) fclose($state["fp"]); 1484 | else $state["result"]["fp"] = $state["fp"]; 1485 | 1486 | return $state["result"]; 1487 | } 1488 | else 1489 | { 1490 | return self::CleanupErrorState($state, array("success" => false, "error" => self::HTTPTranslate("Invalid 'type' in state tracker."), "errorcode" => "invalid_type")); 1491 | } 1492 | } 1493 | 1494 | public static function RawFileSize($fileorname) 1495 | { 1496 | if (is_resource($fileorname)) $fp = $fileorname; 1497 | else 1498 | { 1499 | $fp = @fopen($fileorname, "rb"); 1500 | if ($fp === false) return 0; 1501 | } 1502 | 1503 | if (PHP_INT_SIZE < 8) 1504 | { 1505 | $pos = 0; 1506 | $size = 1073741824; 1507 | fseek($fp, 0, SEEK_SET); 1508 | while ($size > 1) 1509 | { 1510 | if (fseek($fp, $size, SEEK_CUR) === -1) break; 1511 | 1512 | if (fgetc($fp) === false) 1513 | { 1514 | fseek($fp, -$size, SEEK_CUR); 1515 | $size = (int)($size / 2); 1516 | } 1517 | else 1518 | { 1519 | fseek($fp, -1, SEEK_CUR); 1520 | $pos += $size; 1521 | } 1522 | } 1523 | 1524 | if ($size > 1) 1525 | { 1526 | // Unfortunately, fseek() failed for some reason. Going to have to do this the old-fashioned way. 1527 | do 1528 | { 1529 | $data = fread($fp, 10485760); 1530 | if ($data === false) $data = ""; 1531 | $pos += strlen($data); 1532 | } while ($data !== ""); 1533 | } 1534 | else 1535 | { 1536 | while (fgetc($fp) !== false) $pos++; 1537 | } 1538 | } 1539 | else 1540 | { 1541 | fseek($fp, 0, SEEK_END); 1542 | $pos = ftell($fp); 1543 | } 1544 | 1545 | if (!is_resource($fileorname)) fclose($fp); 1546 | 1547 | return $pos; 1548 | } 1549 | 1550 | public static function RetrieveWebpage($url, $options = array()) 1551 | { 1552 | $startts = microtime(true); 1553 | $timeout = (isset($options["timeout"]) ? $options["timeout"] : false); 1554 | 1555 | 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"); 1556 | 1557 | // Process the URL. 1558 | $url = trim($url); 1559 | $url = self::ExtractURL($url); 1560 | 1561 | 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"); 1562 | 1563 | $secure = ($url["scheme"] == "https"); 1564 | $async = (isset($options["async"]) ? $options["async"] : false); 1565 | $protocol = ($secure && !$async ? (isset($options["protocol"]) ? strtolower($options["protocol"]) : "ssl") : "tcp"); 1566 | 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"); 1567 | $host = str_replace(" ", "-", self::HeaderValueCleanup($url["host"])); 1568 | if ($host == "") return array("success" => false, "error" => self::HTTPTranslate("Invalid URL.")); 1569 | $port = ((int)$url["port"] ? (int)$url["port"] : ($secure ? 443 : 80)); 1570 | $defaultport = ((!$secure && $port == 80) || ($secure && $port == 443)); 1571 | $path = ($url["path"] == "" ? "/" : $url["path"]); 1572 | $query = $url["query"]; 1573 | $username = $url["loginusername"]; 1574 | $password = $url["loginpassword"]; 1575 | 1576 | // Cleanup input headers. 1577 | if (!isset($options["headers"])) $options["headers"] = array(); 1578 | $options["headers"] = self::NormalizeHeaders($options["headers"]); 1579 | if (isset($options["rawheaders"])) self::MergeRawHeaders($options["headers"], $options["rawheaders"]); 1580 | 1581 | // Process the proxy URL (if specified). 1582 | $useproxy = (isset($options["proxyurl"]) && trim($options["proxyurl"]) != ""); 1583 | $proxysecure = false; 1584 | $proxyconnect = false; 1585 | $proxydata = ""; 1586 | if ($useproxy) 1587 | { 1588 | $proxyurl = trim($options["proxyurl"]); 1589 | $proxyurl = self::ExtractURL($proxyurl); 1590 | 1591 | $proxysecure = ($proxyurl["scheme"] == "https"); 1592 | 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"); 1593 | $proxyprotocol = ($proxysecure && !$async ? (isset($options["proxyprotocol"]) ? strtolower($options["proxyprotocol"]) : "ssl") : "tcp"); 1594 | 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"); 1595 | $proxyhost = str_replace(" ", "-", self::HeaderValueCleanup($proxyurl["host"])); 1596 | 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"); 1597 | $proxyport = ((int)$proxyurl["port"] ? (int)$proxyurl["port"] : ($proxysecure ? 443 : 80)); 1598 | $proxypath = ($proxyurl["path"] == "" ? "/" : $proxyurl["path"]); 1599 | $proxyusername = $proxyurl["loginusername"]; 1600 | $proxypassword = $proxyurl["loginpassword"]; 1601 | 1602 | // Open a tunnel instead of letting the proxy modify the request (HTTP CONNECT). 1603 | $proxyconnect = (isset($options["proxyconnect"]) && $options["proxyconnect"] ? $options["proxyconnect"] : false); 1604 | if ($proxyconnect) 1605 | { 1606 | $proxydata = "CONNECT " . $host . ":" . $port . " HTTP/1.1\r\n"; 1607 | if (isset($options["headers"]["User-Agent"])) $proxydata .= "User-Agent: " . $options["headers"]["User-Agent"] . "\r\n"; 1608 | $proxydata .= "Host: " . $host . ($defaultport ? "" : ":" . $port) . "\r\n"; 1609 | $proxydata .= "Proxy-Connection: keep-alive\r\n"; 1610 | if ($proxyusername != "") $proxydata .= "Proxy-Authorization: BASIC " . base64_encode($proxyusername . ":" . $proxypassword) . "\r\n"; 1611 | if (!isset($options["proxyheaders"])) $options["proxyheaders"] = array(); 1612 | $options["proxyheaders"] = self::NormalizeHeaders($options["proxyheaders"]); 1613 | if (isset($options["rawproxyheaders"])) self::MergeRawHeaders($options["proxyheaders"], $options["rawproxyheaders"]); 1614 | 1615 | unset($options["proxyheaders"]["Accept-Encoding"]); 1616 | foreach ($options["proxyheaders"] as $name => $val) 1617 | { 1618 | if ($name != "Content-Type" && $name != "Content-Length" && $name != "Proxy-Connection" && $name != "Host") $proxydata .= $name . ": " . $val . "\r\n"; 1619 | } 1620 | 1621 | $proxydata .= "\r\n"; 1622 | if (isset($options["debug_callback"]) && is_callable($options["debug_callback"])) call_user_func_array($options["debug_callback"], array("rawproxyheaders", $proxydata, &$options["debug_callback_opts"])); 1623 | } 1624 | } 1625 | 1626 | // Process the method. 1627 | if (!isset($options["method"])) 1628 | { 1629 | if ((isset($options["write_body_callback"]) && is_callable($options["write_body_callback"])) || isset($options["body"])) $options["method"] = "PUT"; 1630 | else if (isset($options["postvars"]) || (isset($options["files"]) && count($options["files"]))) $options["method"] = "POST"; 1631 | else $options["method"] = "GET"; 1632 | } 1633 | $options["method"] = preg_replace('/[^A-Z]/', "", strtoupper($options["method"])); 1634 | 1635 | // Process the HTTP version. 1636 | if (!isset($options["httpver"])) $options["httpver"] = "1.1"; 1637 | $options["httpver"] = preg_replace('/[^0-9.]/', "", $options["httpver"]); 1638 | 1639 | // Process the request. 1640 | $data = $options["method"] . " "; 1641 | $data .= ($useproxy && !$proxyconnect ? $url["scheme"] . "://" . $host . ":" . $port : "") . $path . ($query != "" ? "?" . $query : ""); 1642 | $data .= " HTTP/" . $options["httpver"] . "\r\n"; 1643 | 1644 | // Process the headers. 1645 | if ($useproxy && !$proxyconnect && $proxyusername != "") $data .= "Proxy-Authorization: BASIC " . base64_encode($proxyusername . ":" . $proxypassword) . "\r\n"; 1646 | if ($username != "") $data .= "Authorization: BASIC " . base64_encode($username . ":" . $password) . "\r\n"; 1647 | $ver = explode(".", $options["httpver"]); 1648 | if (isset($options["headers"]["Host"])) 1649 | { 1650 | $url2 = self::ExtractURL("http://" . $options["headers"]["Host"]); 1651 | $options["headers"]["Host"] = $url2["host"] . (isset($url2["port"]) && $url2["port"] != "" ? ":" . $url2["port"] : ""); 1652 | } 1653 | if ((int)$ver[0] > 1 || ((int)$ver[0] == 1 && (int)$ver[1] >= 1)) 1654 | { 1655 | if (!isset($options["headers"]["Host"])) $options["headers"]["Host"] = $host . ($defaultport ? "" : ":" . $port); 1656 | $data .= "Host: " . $options["headers"]["Host"] . "\r\n"; 1657 | } 1658 | 1659 | if (!isset($options["headers"]["Connection"])) $options["headers"]["Connection"] = "close"; 1660 | $data .= "Connection: " . $options["headers"]["Connection"] . "\r\n"; 1661 | 1662 | foreach ($options["headers"] as $name => $val) 1663 | { 1664 | if ($name != "Content-Type" && $name != "Content-Length" && $name != "Connection" && $name != "Host") $data .= $name . ": " . $val . "\r\n"; 1665 | } 1666 | 1667 | if (isset($options["files"]) && !count($options["files"])) unset($options["files"]); 1668 | 1669 | // Process the body. 1670 | $mime = ""; 1671 | $body = ""; 1672 | $bodysize = 0; 1673 | if (isset($options["write_body_callback"]) && is_callable($options["write_body_callback"])) 1674 | { 1675 | if (isset($options["headers"]["Content-Type"])) $data .= "Content-Type: " . $options["headers"]["Content-Type"] . "\r\n"; 1676 | 1677 | call_user_func_array($options["write_body_callback"], array(&$body, &$bodysize, &$options["write_body_callback_opts"])); 1678 | } 1679 | else if (isset($options["body"])) 1680 | { 1681 | if (isset($options["headers"]["Content-Type"])) $data .= "Content-Type: " . $options["headers"]["Content-Type"] . "\r\n"; 1682 | 1683 | $body = $options["body"]; 1684 | $bodysize = strlen($body); 1685 | unset($options["body"]); 1686 | } 1687 | else if ((isset($options["files"]) && count($options["files"])) || (isset($options["headers"]["Content-Type"]) && stripos($options["headers"]["Content-Type"], "multipart/form-data") !== false)) 1688 | { 1689 | $mime = "--------" . substr(sha1(uniqid(mt_rand(), true)), 0, 25); 1690 | $data .= "Content-Type: multipart/form-data; boundary=" . $mime . "\r\n"; 1691 | if (isset($options["postvars"])) 1692 | { 1693 | foreach ($options["postvars"] as $name => $val) 1694 | { 1695 | $name = self::HeaderValueCleanup($name); 1696 | $name = str_replace("\"", "", $name); 1697 | 1698 | if (!is_array($val)) 1699 | { 1700 | if (is_string($val) || is_numeric($val)) $val = array($val); 1701 | 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)); 1702 | } 1703 | 1704 | foreach ($val as $val2) 1705 | { 1706 | $body .= "--" . $mime . "\r\n"; 1707 | $body .= "Content-Disposition: form-data; name=\"" . $name . "\"\r\n"; 1708 | $body .= "\r\n"; 1709 | $body .= $val2 . "\r\n"; 1710 | } 1711 | } 1712 | 1713 | unset($options["postvars"]); 1714 | } 1715 | 1716 | $bodysize = strlen($body); 1717 | 1718 | // Only count the amount of data to send. 1719 | if (!isset($options["files"])) $options["files"] = array(); 1720 | foreach ($options["files"] as $num => $info) 1721 | { 1722 | $name = self::HeaderValueCleanup($info["name"]); 1723 | $name = str_replace("\"", "", $name); 1724 | $filename = self::FilenameSafe(self::ExtractFilename($info["filename"])); 1725 | $type = self::HeaderValueCleanup($info["type"]); 1726 | 1727 | $body2 = "--" . $mime . "\r\n"; 1728 | $body2 .= "Content-Disposition: form-data; name=\"" . $name . "\"; filename=\"" . $filename . "\"\r\n"; 1729 | $body2 .= "Content-Type: " . $type . "\r\n"; 1730 | $body2 .= "\r\n"; 1731 | 1732 | $info["filesize"] = (isset($info["datafile"]) ? self::RawFileSize($info["datafile"]) : strlen($info["data"])); 1733 | $bodysize += strlen($body2) + $info["filesize"] + 2; 1734 | 1735 | $options["files"][$num] = $info; 1736 | } 1737 | 1738 | $body2 = "--" . $mime . "--\r\n"; 1739 | $bodysize += strlen($body2); 1740 | } 1741 | else 1742 | { 1743 | if (isset($options["postvars"])) 1744 | { 1745 | foreach ($options["postvars"] as $name => $val) 1746 | { 1747 | $name = self::HeaderValueCleanup($name); 1748 | 1749 | if (!is_array($val)) 1750 | { 1751 | if (is_string($val) || is_numeric($val)) $val = array($val); 1752 | 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)); 1753 | } 1754 | 1755 | foreach ($val as $val2) $body .= ($body != "" ? "&" : "") . urlencode($name) . "=" . urlencode($val2); 1756 | } 1757 | 1758 | unset($options["postvars"]); 1759 | } 1760 | 1761 | if ($body != "") $data .= "Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n"; 1762 | 1763 | $bodysize = strlen($body); 1764 | } 1765 | if (($bodysize === false && strlen($body) > 0) || ($bodysize !== false && $bodysize < strlen($body))) $bodysize = strlen($body); 1766 | 1767 | // Finalize the headers. 1768 | if ($bodysize === false) $data .= "Transfer-Encoding: chunked\r\n"; 1769 | else if ($bodysize > 0 || $body != "" || $options["method"] == "POST") $data .= "Content-Length: " . $bodysize . "\r\n"; 1770 | $data .= "\r\n"; 1771 | if (isset($options["debug_callback"]) && is_callable($options["debug_callback"])) call_user_func_array($options["debug_callback"], array("rawheaders", $data, &$options["debug_callback_opts"])); 1772 | $rawheadersize = strlen($data); 1773 | 1774 | // Finalize the initial data to be sent. 1775 | $data .= $body; 1776 | if ($bodysize !== false) $bodysize -= strlen($body); 1777 | $body = ""; 1778 | $result = array("success" => true, "rawsendsize" => 0, "rawsendheadersize" => $rawheadersize, "rawrecvsize" => 0, "rawrecvheadersize" => 0, "startts" => $startts); 1779 | $debug = (isset($options["debug"]) && $options["debug"]); 1780 | if ($debug) 1781 | { 1782 | $result["rawsend"] = ""; 1783 | $result["rawrecv"] = ""; 1784 | } 1785 | 1786 | if ($timeout !== false && self::GetTimeLeft($startts, $timeout) == 0) return array("success" => false, "error" => self::HTTPTranslate("HTTP timeout exceeded."), "errorcode" => "timeout_exceeded"); 1787 | 1788 | // Connect to the target server. 1789 | $errornum = 0; 1790 | $errorstr = ""; 1791 | if (isset($options["fp"]) && is_resource($options["fp"])) 1792 | { 1793 | $fp = $options["fp"]; 1794 | unset($options["fp"]); 1795 | 1796 | $useproxy = false; 1797 | $proxyconnect = false; 1798 | $proxydata = ""; 1799 | } 1800 | else if ($useproxy) 1801 | { 1802 | if (!isset($options["proxyconnecttimeout"])) $options["proxyconnecttimeout"] = 10; 1803 | $timeleft = self::GetTimeLeft($startts, $timeout); 1804 | if ($timeleft !== false) $options["proxyconnecttimeout"] = min($options["proxyconnecttimeout"], $timeleft); 1805 | if (!function_exists("stream_socket_client")) 1806 | { 1807 | if ($debug) $fp = fsockopen($proxyprotocol . "://" . $proxyhost, $proxyport, $errornum, $errorstr, $options["proxyconnecttimeout"]); 1808 | else $fp = @fsockopen($proxyprotocol . "://" . $proxyhost, $proxyport, $errornum, $errorstr, $options["proxyconnecttimeout"]); 1809 | } 1810 | else 1811 | { 1812 | $context = @stream_context_create(); 1813 | if (isset($options["source_ip"])) $context["socket"] = array("bindto" => $options["source_ip"] . ":0"); 1814 | if ($proxysecure) 1815 | { 1816 | if (!isset($options["proxysslopts"]) || !is_array($options["proxysslopts"])) $options["proxysslopts"] = self::GetSafeSSLOpts(); 1817 | self::ProcessSSLOptions($options, "proxysslopts", $host); 1818 | foreach ($options["proxysslopts"] as $key => $val) @stream_context_set_option($context, "ssl", $key, $val); 1819 | } 1820 | else if ($secure) 1821 | { 1822 | if (!isset($options["sslopts"]) || !is_array($options["sslopts"])) 1823 | { 1824 | $options["sslopts"] = self::GetSafeSSLOpts(); 1825 | $options["sslopts"]["auto_peer_name"] = true; 1826 | } 1827 | 1828 | self::ProcessSSLOptions($options, "sslopts", $host); 1829 | foreach ($options["sslopts"] as $key => $val) @stream_context_set_option($context, "ssl", $key, $val); 1830 | } 1831 | 1832 | if ($debug) $fp = stream_socket_client($proxyprotocol . "://" . $proxyhost . ":" . $proxyport, $errornum, $errorstr, $options["proxyconnecttimeout"], ($async ? STREAM_CLIENT_ASYNC_CONNECT : STREAM_CLIENT_CONNECT), $context); 1833 | else $fp = @stream_socket_client($proxyprotocol . "://" . $proxyhost . ":" . $proxyport, $errornum, $errorstr, $options["proxyconnecttimeout"], ($async ? STREAM_CLIENT_ASYNC_CONNECT : STREAM_CLIENT_CONNECT), $context); 1834 | } 1835 | 1836 | 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"); 1837 | } 1838 | else 1839 | { 1840 | if (!isset($options["connecttimeout"])) $options["connecttimeout"] = 10; 1841 | $timeleft = self::GetTimeLeft($startts, $timeout); 1842 | if ($timeleft !== false) $options["connecttimeout"] = min($options["connecttimeout"], $timeleft); 1843 | if (!function_exists("stream_socket_client")) 1844 | { 1845 | if ($debug) $fp = fsockopen($protocol . "://" . $host, $port, $errornum, $errorstr, $options["connecttimeout"]); 1846 | else $fp = @fsockopen($protocol . "://" . $host, $port, $errornum, $errorstr, $options["connecttimeout"]); 1847 | } 1848 | else 1849 | { 1850 | $context = @stream_context_create(); 1851 | if (isset($options["source_ip"])) $context["socket"] = array("bindto" => $options["source_ip"] . ":0"); 1852 | if ($secure) 1853 | { 1854 | if (!isset($options["sslopts"]) || !is_array($options["sslopts"])) 1855 | { 1856 | $options["sslopts"] = self::GetSafeSSLOpts(); 1857 | $options["sslopts"]["auto_peer_name"] = true; 1858 | } 1859 | 1860 | self::ProcessSSLOptions($options, "sslopts", $host); 1861 | foreach ($options["sslopts"] as $key => $val) @stream_context_set_option($context, "ssl", $key, $val); 1862 | } 1863 | 1864 | if ($debug) $fp = stream_socket_client($protocol . "://" . $host . ":" . $port, $errornum, $errorstr, $options["connecttimeout"], ($async ? STREAM_CLIENT_ASYNC_CONNECT : STREAM_CLIENT_CONNECT), $context); 1865 | else $fp = @stream_socket_client($protocol . "://" . $host . ":" . $port, $errornum, $errorstr, $options["connecttimeout"], ($async ? STREAM_CLIENT_ASYNC_CONNECT : STREAM_CLIENT_CONNECT), $context); 1866 | } 1867 | 1868 | 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"); 1869 | } 1870 | 1871 | if (function_exists("stream_set_blocking")) @stream_set_blocking($fp, ($async ? 0 : 1)); 1872 | 1873 | // Initialize the connection request state array. 1874 | $state = array( 1875 | "fp" => $fp, 1876 | "type" => "request", 1877 | "async" => $async, 1878 | "debug" => $debug, 1879 | "startts" => $startts, 1880 | "timeout" => $timeout, 1881 | "waituntil" => -1.0, 1882 | "mime" => $mime, 1883 | "data" => $data, 1884 | "bodysize" => $bodysize, 1885 | "chunked" => ($bodysize === false), 1886 | "secure" => $secure, 1887 | "useproxy" => $useproxy, 1888 | "proxysecure" => $proxysecure, 1889 | "proxyconnect" => $proxyconnect, 1890 | "proxydata" => $proxydata, 1891 | "currentfile" => false, 1892 | 1893 | "state" => "connecting", 1894 | 1895 | "options" => $options, 1896 | "result" => $result, 1897 | "close" => ($options["headers"]["Connection"] === "close"), 1898 | "nextread" => "", 1899 | "client" => true 1900 | ); 1901 | 1902 | // Return the state for async calls. Caller must call ProcessState(). 1903 | if ($state["async"]) return array("success" => true, "state" => $state); 1904 | 1905 | // Run through all of the valid states and return the result. 1906 | return self::ProcessState($state); 1907 | } 1908 | } 1909 | ?> --------------------------------------------------------------------------------