request($url, array('func_name',xxx), $datas, $is_xmlrpc, $keepalive_min_timeout); // $url: the web script URL. // $datas: string to send in http body (xml, xml_rpc or POST data) // $is_xmlrpc: true if it's an xml or xml-rpc request, false if it's a // standard html GET or POST // $keepalive_min_timeout: minimal value of server keepalive timeout to // send a keepalive request, // else make a request with close connection. // func_name is the callback function name, which will be called this way: // func_name(array('Code'=>code,'Reason'=>reason,'Headers'=>headers,'Message'=>message), xxx) // where: // xxx is the same as given previously in callback description. // code is the returned http code // (http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6) // reason is the returned http reason // (http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6) // headers are the http headers of the reply // message is the returned text body // // IMPORTANT: to have this work, the main part of your program must include a // $_webaccess->select() call periodically, which work exactly like stream_select(). // This is because the send and receive are asynchronous and will be completed // later in the select() when datas are ready to be received and sent. // This class can be used to make a synchronous query too. For that use null // for the callback, so make the request this way: // $response = $_webaccess->request($url, null, $datas, $is_xmlrpc, $keepalive_min_timeout); // where $response is an array('Code'=>code,'Reason'=>reason,'Headers'=>headers,'Message'=>message) // like the one passed to the callback function of the asynchronous request. // If you use only synchronous queries then there is no need to call select() // as the function will return when the reply will be fully returned. // If the connection itself fail, the array response will include a 'Error' string. // Other functions: // list($host, $port, $path) = getHostPortPath($url); // gzdecode() workaround global $_web_access_compress_xmlrpc_request, $_web_access_compress_reply, $_web_access_keepalive, $_web_access_keepalive_timeout, $_web_access_keepalive_max, $_web_access_retry_timeout, $_web_access_retry_timeout_max, $_web_access_post_xmlrpc; // Will compress xmlrpc request ('never','accept','force','force-gzip','force-deflate') // If set to 'accept' the first request will be made without, and the eventual // 'Accept-Encoding' in reply will permit to decide if request compression can // be used (and if gzip or deflate) $_web_access_compress_xmlrpc_request = 'accept'; // Will ask server for compressed reply (false, true) // If true then will add a 'Accept-Encoding' header to tell the server to // compress the reply if it supports it. $_web_access_compress_reply = true; // Keep alive connection ? else close it after the reply. // Unless false, first request will be with keepalive, to get server timeout // and max values after timeout will be compared with the request // $keepalive_min_timeout value to decide if keepalive have to be used or not. // Note that Apache2 timeout is short (about 15s). // The classes will open, re-open or use existing connection as needed. $_web_access_keepalive = true; // timeout(s) without request before close, for keepalive $_web_access_keepalive_timeout = 600; // max requests before close, for keepalive $_web_access_keepalive_max = 2000; // For asynchronous call, in case of error, timeout before retrying. // It will be x2 for each error (on request or auto retry) until max, // then stop automatic retry, and next request calls will return false. // When stopped, a retry() or synchronous request will force a retry. $_web_access_retry_timeout = 20; $_web_access_retry_timeout_max = 5*60; // Use text/html with xmlrpc=, instead of of pure text/xml request (false, true) // Standard xml-rpc use pure text/xml request, where the xml is simply the body // of the http request (and it's how the xml-rpc reply will be made). As a // facility Dedimania also supports to get the xml in a html GET or POST, // where xmlrpc= will contain a urlsafe base64 of the xml. Default to false, // so use pure text/xml. $_web_access_post_xmlrpc = false; // Note that in each request the text/xml or xmlrpc= will be used only if // $is_xmlrpc is true. If false then the request will be a standard // application/x-www-form-urlencoded html GET or POST request; in that case // you have to build the URL (GET) and/or body data (POST) yourself. // If $is_xmlrpc is a string, then it's used as the Content-type: value. require_once('includes/urlsafebase64.php'); class Webaccess { var $_WebaccessList; function Webaccess() { $this->_WebaccessList = array(); } function request($url, $callback, $datas, $is_xmlrpc = false, $keepalive_min_timeout = 300, $opentimeout = 3, $waittimeout = 5, $agent = 'XMLaccess') { global $aseco, $_web_access_keepalive, $_web_access_keepalive_timeout, $_web_access_keepalive_max; list($host, $port, $path) = getHostPortPath($url); if ($host === false) $aseco->console('Webaccess request(): Bad URL: ' . $url); else { $server = $host . ':' . $port; // create object if needed if (!isset($this->_WebaccessList[$server]) || $this->_WebaccessList[$server] === null) { $this->_WebaccessList[$server] = new WebaccessUrl($this, $host, $port, $_web_access_keepalive, $_web_access_keepalive_timeout, $_web_access_keepalive_max, $agent); } // increase the default timeout for sync/wait request if ($callback == null && $waittimeout == 5) $waittimeout = 12; // call request if ($this->_WebaccessList[$server] !== null) { $query = array('Path' => $path, 'Callback' => $callback, 'QueryData' => $datas, 'IsXmlrpc' => $is_xmlrpc, 'KeepaliveMinTimeout' => $keepalive_min_timeout, 'OpenTimeout' => $opentimeout, 'WaitTimeout' => $waittimeout ); return $this->_WebaccessList[$server]->request($query); } } return false; } // request function retry($url) { global $aseco; list($host, $port, $path) = getHostPortPath($url); if ($host === false) { $aseco->console('Webaccess retry(): Bad URL: ' . $url); } else { $server = $host . ':' . $port; if (isset($this->_WebaccessList[$server])) $this->_WebaccessList[$server]->retry(); } } // retry function select(&$read, &$write, &$except, $tv_sec, $tv_usec = 0) { $timeout = (int)($tv_sec*1000000 + $tv_usec); if ($read == null) $read = array(); if ($write == null) $write = array(); if ($except == null) $except = array(); $read = $this->_getWebaccessReadSockets($read); $write = $this->_getWebaccessWriteSockets($write); //$except = $this->_getWebaccessReadSockets($except); //print_r($this->_WebaccessList); //print_r($read); //print_r($write); //print_r($except); // if no socket to select then return if (count($read) + count($write) + count($except) == 0) { // sleep the asked timeout... if ($timeout > 1000) usleep($timeout); return 0; } $utime = (int)(microtime(true)*1000000); $nb = @stream_select($read, $write, $except, $tv_sec, $tv_usec); if ($nb === false) { // in case stream_select "forgot" to wait, sleep the remaining asked timeout... $dtime = (int)(microtime(true)*1000000) - $utime; $timeout -= $dtime; if ($timeout > 1000) usleep($timeout); return false; } $this->_manageWebaccessSockets($read, $write, $except); // workaround for stream_select bug with amd64, replace $nb with sum of arrays return count($read) + count($write) + count($except); } // select private function _manageWebaccessSockets(&$receive, &$send, &$except) { // send pending data on all webaccess sockets if (is_array($send) && count($send) > 0) { foreach ($send as $key => $socket) { $i = $this->_findWebaccessSocket($socket); if ($i !== false) { if (isset($this->_WebaccessList[$i]->_spool[0]['State']) && $this->_WebaccessList[$i]->_spool[0]['State'] == 'OPEN') $this->_WebaccessList[$i]->_open(); else $this->_WebaccessList[$i]->_send(); unset($send[$key]); } } } // read data from all needed webaccess sockets if (is_array($receive) && count($receive) > 0) { foreach ($receive as $key => $socket) { $i = $this->_findWebaccessSocket($socket); if ($i !== false) { $this->_WebaccessList[$i]->_receive(); unset($receive[$key]); } } } } // _manageWebaccessSockets private function _findWebaccessSocket($socket) { foreach ($this->_WebaccessList as $key => $wau) { if ($wau->_socket == $socket) return $key; } return false; } // _findWebaccessSocket private function _getWebaccessReadSockets($socks) { foreach ($this->_WebaccessList as $key => $wau) { if ($wau->_state == 'OPENED' && $wau->_socket) $socks[] = $wau->_socket; } return $socks; } // _getWebaccessReadSockets private function _getWebaccessWriteSockets($socks) { foreach ($this->_WebaccessList as $key => $wau) { if (isset($wau->_spool[0]['State']) && ($wau->_spool[0]['State'] == 'OPEN' || $wau->_spool[0]['State'] == 'BAD' || $wau->_spool[0]['State'] == 'SEND')) { if (($wau->_state == 'CLOSED' || $wau->_state == 'BAD') && !$wau->_socket) $wau->_open(); if ($wau->_state == 'OPENED' && $wau->_socket) $socks[] = $wau->_socket; } } return $socks; } // _getWebaccessWriteSockets function getAllSpools() { $num = 0; $bad = 0; foreach ($this->_WebaccessList as $key => $wau) { if ($wau->_state == 'OPENED' || $wau->_state == 'CLOSED') $num += count($wau->_spool); elseif ($wau->_state == 'BAD') $bad += count($wau->_spool); } return array($num, $bad); } // getAllSpools } // class Webaccess // useful data to handle received headers $_wa_header_separator = array('cookie' => ';', 'set-cookie' => ';'); $_wa_header_multi = array('set-cookie' => true); class WebaccessUrl { //----------------------------- // Fields //----------------------------- var $wa; var $_host; var $_port; var $_compress_request; var $_socket; var $_state; var $_keepalive; var $_keepalive_timeout; var $_keepalive_max; var $_serv_keepalive_timeout; var $_serv_keepalive_max; var $_spool; var $_wait; var $_response; var $_query_num; var $_request_time; var $_cookies; var $_webaccess_str; var $_bad_time; var $_bad_timeout; var $_read_time; var $_agent; // $_state values: // 'OPENED' : socket is opened // 'CLOSED' : socket is closed (asked, completed, or closed by server) // 'BAD' : socket is closed, bad/error or beginning state // $query['State'] values: (note: $query is added in $_spool, so $this->_spool[0] is the first $query to handle) // 'BAD' : bad/error or beginning state // 'OPEN' : should prepare request data then send them // 'SEND' : request data are prepared, send them // 'RECEIVE': request data are sent, receive reply data // 'DONE' : request completed //----------------------------- // Methods //----------------------------- function WebaccessUrl(&$wa, $host, $port, $keepalive = true, $keepalive_timeout = 600, $keepalive_max = 300, $agent = 'XMLaccess') { global $_web_access_compress_xmlrpc_request, $_web_access_retry_timeout; $this->wa = &$wa; $this->_host = $host; $this->_port = $port; $this->_webaccess_str = 'Webaccess (' . $this->_host . ':' . $this->_port . '): '; $this->_agent = $agent; // request compression setting if ($_web_access_compress_xmlrpc_request == 'accept') $this->_compress_request = 'accept'; elseif ($_web_access_compress_xmlrpc_request == 'force') { if (function_exists('gzencode')) $this->_compress_request = 'gzip'; elseif (function_exists('gzdeflate')) $this->_compress_request = 'deflate'; else $this->_compress_request = false; } elseif ($_web_access_compress_xmlrpc_request == 'force-gzip' && function_exists('gzencode')) $this->_compress_request = 'gzip'; elseif ($_web_access_compress_xmlrpc_request == 'force-deflate' && function_exists('gzdeflate')) $this->_compress_request = 'deflate'; else $this->_compress_request = false; $this->_socket = null; $this->_state = 'CLOSED'; $this->_keepalive = $keepalive; $this->_keepalive_timeout = $keepalive_timeout; $this->_keepalive_max = $keepalive_max; $this->_serv_keepalive_timeout = $keepalive_timeout; $this->_serv_keepalive_max = $keepalive_max; $this->_spool = array(); $this->_wait = false; $this->_response = ''; $this->_query_num = 0; $this->_query_time = time(); $this->_cookies = array(); $this->_bad_time = time(); $this->_bad_timeout = 0; $this->_read_time = 0; } // WebaccessUrl // put connection in BAD state function _bad($errstr, $isbad = true) { global $aseco, $_web_access_retry_timeout; $aseco->console($this->_webaccess_str . $errstr); $this->infos(); if ($this->_socket) @fclose($this->_socket); $this->_socket = null; if ($isbad) { if (isset($this->_spool[0]['State'])) $this->_spool[0]['State'] = 'BAD'; $this->_state = 'BAD'; $this->_bad_time = time(); if ($this->_bad_timeout < $_web_access_retry_timeout) $this->_bad_timeout = $_web_access_retry_timeout; else $this->_bad_timeout *= 2; } else { if (isset($this->_spool[0]['State'])) $this->_spool[0]['State'] = 'OPEN'; $this->_state = 'CLOSED'; } $this->_callCallback($this->_webaccess_str . $errstr); } // _bad function retry() { global $_web_access_retry_timeout; if ($this->_state == 'BAD') { $this->_bad_time = time(); $this->_bad_timeout = 0; } } // retry //$query = array('Path' => $path, // 'Callback' => $callback, // 'QueryData' => $datas, // 'IsXmlrpc' => $is_xmlrpc, // 'KeepaliveMinTimeout' => $keepalive_min_timeout, // 'OpenTimeout' => $opentimeout, // 'WaitTimeout' => $waittimeout ); // will add: 'State', 'HDatas', 'Datas', 'DatasSize', 'DatasSent', // 'Response', 'ResponseSize', 'Headers', 'Close', 'Times' function request(&$query) { global $aseco, $_web_access_compress_reply, $_web_access_post_xmlrpc, $_web_access_retry_timeout, $_web_access_retry_timeout_max; $query['State'] = 'BAD'; $query['HDatas'] = ''; $query['Datas'] = ''; $query['DatasSize'] = 0; $query['DatasSent'] = 0; $query['Response'] = ''; $query['ResponseSize'] = 0; $query['Headers'] = array(); $query['Close'] = false; $query['Times'] = array('open' => array(-1.0,-1.0), 'send' => array(-1.0,-1.0), 'receive' => array(-1.0,-1.0,0)); // if asynch, in error, and maximal timeout, then forget the request and return false if (($query['Callback'] != null) && ($this->_state == 'BAD')) { if ($this->_bad_timeout > $_web_access_retry_timeout_max) { $aseco->console($this->_webaccess_str . 'Request refused for consecutive errors (' . $this->_bad_timeout . ' / ' . $_web_access_retry_timeout_max . ')'); return false; } else { // if not max then accept the request and try a request (minimum $_web_access_retry_timeout/2 after previous try) $time = time(); $timeout = ($this->_bad_timeout / 2) - ($time - $this->_bad_time); if ($timeout < 0) $timeout = 0; $this->_bad_time = $time - $this->_bad_timeout + $timeout; } } // build data to send if (($query['Callback'] == null) || (is_array($query['Callback']) && isset($query['Callback'][0]) && is_callable($query['Callback'][0]))) { if (is_string($query['QueryData']) && strlen($query['QueryData']) > 0) { $msg = "POST " . $query['Path'] . " HTTP/1.1\r\n"; $msg .= "Host: " . $this->_host . "\r\n"; $msg .= "User-Agent: " . $this->_agent . "\r\n"; $msg .= "Cache-Control: no-cache\r\n"; if ($_web_access_compress_reply) { // ask compression of response if gzdecode() and/or gzinflate() is available if (function_exists('gzdecode') && function_exists('gzinflate')) $msg .= "Accept-Encoding: deflate, gzip\r\n"; elseif (function_exists('gzdecode')) $msg .= "Accept-Encoding: gzip\r\n"; elseif (function_exists('gzinflate')) $msg .= "Accept-Encoding: deflate\r\n"; } //echo "\nData:\n\n" . $query['QueryData'] . "\n"; if ($query['IsXmlrpc'] === true) { if ($_web_access_post_xmlrpc) { $msg .= "Content-type: application/x-www-form-urlencoded; charset=UTF-8\r\n"; //echo "\n=========================== Data =================================\n\n" . $datas . "\n"; //$d2 = urlsafe_base64_encode($datas); //$d3 = urlsafe_base64_decode($d2); //echo "\n--------------------------- Data ---------------------------------\n\n" . $d3 . "\n"; $query['QueryData'] = 'xmlrpc=' . urlsafe_base64_encode($query['QueryData']); } else { $msg .= "Content-type: text/xml; charset=UTF-8\r\n"; } if ($this->_compress_request == 'gzip' && function_exists('gzencode')) { $msg .= "Content-Encoding: gzip\r\n"; $query['QueryData'] = gzencode($query['QueryData']); } elseif ($this->_compress_request == 'deflate' && function_exists('gzdeflate')) { $msg .= "Content-Encoding: deflate\r\n"; $query['QueryData'] = gzdeflate($query['QueryData']); } } elseif (is_string($query['IsXmlrpc'])) { $msg .= "Content-type: " . $query['IsXmlrpc'] . "\r\n"; $msg .= "Accept: */*\r\n"; } else { $msg .= "Content-type: application/x-www-form-urlencoded; charset=UTF-8\r\n"; } $msg .= "Content-length: " . strlen($query['QueryData']) . "\r\n"; $query['HDatas'] = $msg; $query['State'] = 'OPEN'; $query['Retries'] = 0; //print_r($msg); // add the query in spool $this->_spool[] = &$query; if ($query['Callback'] == null) { $this->_wait = true; $this->_open($query['OpenTimeout'], $query['WaitTimeout']); // wait more in not callback mode $this->_spool = array(); $this->_wait = false; return $query['Response']; } else $this->_open(); } else { $aseco->console($this->_webaccess_str . 'Bad data'); return false; } } else { $aseco->console($this->_webaccess_str . 'Bad callback function: ' . $query['Callback']); return false; } return true; } // request // open the socket (close it before if needed) private function _open_socket($opentimeout = 0.0) { global $aseco; // if socket not opened, then open it (2 tries) if (!$this->_socket || $this->_state != 'OPENED') { $time = microtime(true); $this->_spool[0]['Times']['open'][0] = $time; $errno = ''; $errstr = ''; $this->_socket = @fsockopen($this->_host, $this->_port, $errno, $errstr, 1.8); // first try if (!$this->_socket) { if ($opentimeout >= 1.0) $this->_socket = @fsockopen($this->_host, $this->_port, $errno, $errstr, $opentimeout); if (!$this->_socket) { $this->_bad('Error(' . $errno . ') ' . $errstr . ', connection failed!'); return; } } $this->_state = 'OPENED'; //$aseco->console($this->_webaccess_str . 'connection opened!'); // new socket connection: reset all pending request original values for ($i = 0; $i < count($this->_spool); $i++) { $this->_spool[$i]['State'] = 'OPEN'; $this->_spool[$i]['DatasSent'] = 0; $this->_spool[$i]['Response'] = ''; $this->_spool[$i]['Headers'] = array(); } $this->_response = ''; $this->_query_num = 0; $this->_query_time = time(); $time = microtime(true); $this->_spool[0]['Times']['open'][1] = $time - $this->_spool[0]['Times']['open'][0]; } } // _open_socket // open the connection (if not already opened) and send function _open($opentimeout = 0.0, $waittimeout = 5.0) { global $aseco, $_web_access_retry_timeout_max; if (!isset($this->_spool[0]['State'])) return false; $time = time(); // if asynch, in error, then return false until timeout or if >max) if (!$this->_wait && $this->_state == 'BAD' && (($this->_bad_timeout > $_web_access_retry_timeout_max) || (($time - $this->_bad_time) < $this->_bad_timeout))) { //$aseco->console($this->_webaccess_str . 'wait to retry (' . ($time - $this->_bad_time) . ' / ' . $this->_bad_timeout . ')'); return false; } // if the socket is probably in timeout, close it if ($this->_socket && $this->_state == 'OPENED' && ($this->_serv_keepalive_timeout <= ($time - $this->_query_time))) { //$aseco->console($this->_webaccess_str . 'timeout, close it!'); $this->_state = 'CLOSED'; @fclose($this->_socket); $this->_socket = null; } // if socket is not opened, open it if (!$this->_socket || $this->_state != 'OPENED') $this->_open_socket($opentimeout); // if socket is open, send data if possible if ($this->_socket) { $this->_read_time = microtime(true); // if wait (synchronous query) then go on all pending write/read until the last if ($this->_wait) { @stream_set_timeout($this->_socket, 0, 10000); // timeout 10 ms while (isset($this->_spool[0]['State']) && ($this->_spool[0]['State'] == 'OPEN' || $this->_spool[0]['State'] == 'SEND' || $this->_spool[0]['State'] == 'RECEIVE')) { //echo 'State=' . $this->_spool[0]['State'] . " (" . count($this->_spool) . ")\n"; if (!$this->_socket || $this->_state != 'OPENED') $this->_open_socket($opentimeout); if ($this->_spool[0]['State'] == 'OPEN') { $time = microtime(true); $this->_spool[0]['Times']['send'][0] = $time; $this->_send($waittimeout); } elseif ($this->_spool[0]['State'] == 'SEND') $this->_send($waittimeout); elseif ($this->_spool[0]['State'] == 'RECEIVE') $this->_receive($waittimeout*4); // if timeout then error if (($difftime = microtime(true) - $this->_read_time) > $waittimeout) { $this->_bad('Request timeout, in _open (' . round($difftime) . ' > ' . $waittimeout . 's) state=' . $this->_spool[0]['State']); return; } } if ($this->_socket) @stream_set_timeout($this->_socket, 0, 2000); // timeout 2 ms } // else just do a send on the current elseif (isset($this->_spool[0]['State']) && $this->_spool[0]['State'] == 'OPEN') { @stream_set_timeout($this->_socket, 0, 2000); // timeout 2 ms $this->_send($waittimeout); } } } // _open function _send($waittimeout = 20) { if (!isset($this->_spool[0]['State'])) return; // if OPEN then become SEND if ($this->_spool[0]['State'] == 'OPEN') { $this->_spool[0]['State'] = 'SEND'; $time = microtime(true); $this->_spool[0]['Times']['send'][0] = $time; $this->_spool[0]['Response'] = ''; $this->_spool[0]['Headers'] = array(); // finish to prepare header and data to send $msg = $this->_spool[0]['HDatas']; if (!$this->_keepalive || ($this->_spool[0]['KeepaliveMinTimeout'] < 0) || ($this->_serv_keepalive_timeout < $this->_spool[0]['KeepaliveMinTimeout']) || ($this->_serv_keepalive_max <= ($this->_query_num + 2)) || ($this->_serv_keepalive_timeout <= (time() - $this->_query_time + 2))) { $msg .= "Connection: close\r\n"; $this->_spool[0]['Close'] = true; } else { $msg .= 'Keep-Alive: timeout=' . $this->_keepalive_timeout . ', max=' . $this->_keepalive_max . "\r\nConnection: Keep-Alive\r\n"; } // add cookie header if (count($this->_cookies) > 0) { $cookie_msg = ''; $sep = ''; foreach ($this->_cookies as $name => $cookie) { if (!isset($cookie['path']) || strncmp($this->_spool[0]['Path'], $cookie['path'], strlen($cookie['path'])) == 0) { $cookie_msg .= $sep . $name . '=' . $cookie['Value']; $sep = '; '; } } if ($cookie_msg != '') $msg .= "Cookie: $cookie_msg\r\n"; } $msg .= "\r\n"; $msg .= $this->_spool[0]['QueryData']; $this->_spool[0]['Datas'] = $msg; $this->_spool[0]['DatasSize'] = strlen($msg); $this->_spool[0]['DatasSent'] = 0; //print_r($msg); } // if not SEND then stop if ($this->_spool[0]['State'] != 'SEND') return; do { $sent = @stream_socket_sendto($this->_socket, substr($this->_spool[0]['Datas'], $this->_spool[0]['DatasSent'], ($this->_spool[0]['DatasSize'] - $this->_spool[0]['DatasSent']))); if ($sent == false) { $time = microtime(true); $this->_spool[0]['Times']['send'][1] = $time - $this->_spool[0]['Times']['send'][0]; //var_dump($this->_spool[0]['Datas']); $this->_bad('Error(' . $errno . ') ' . $errstr . ', could not send data! (' . $sent . ' / ' . ($this->_spool[0]['DatasSize'] - $this->_spool[0]['DatasSent']) . ', ' . $this->_spool[0]['DatasSent'] . ' / ' . $this->_spool[0]['DatasSize'] . ')'); if ($this->_wait) return; break; } else { $this->_spool[0]['DatasSent'] += $sent; if ($this->_spool[0]['DatasSent'] >= $this->_spool[0]['DatasSize']) { // All is sent, prepare to receive the reply $this->_query_num++; $this->_query_time = time(); $time = microtime(true); $this->_spool[0]['Times']['send'][1] = $time - $this->_spool[0]['Times']['send'][0]; //@stream_set_blocking($this->_socket, 0); $this->_spool[0]['State'] = 'RECEIVE'; $this->_spool[0]['Times']['receive'][0] = $time; } // if timeout then error elseif (($difftime = microtime(true) - $this->_read_time) > $waittimeout) { $this->_bad('Request timeout, in _send (' . round($difftime) . ' > ' . $waittimeout . 's)'); } } // if not async-callback then continue until all is sent } while ($this->_wait && isset($this->_spool[0]['State']) && ($this->_spool[0]['State'] == 'SEND')); } // _send function _receive($waittimeout = 40) { global $aseco, $_Webaccess_last_response; if (!$this->_socket || $this->_state != 'OPENED') return; $state = false; $time0 = microtime(true); $timeout = ($this->_wait) ? $waittimeout : 0; do { $r = array($this->_socket); $w = null; $e = null; $nb = @stream_select($r, $w, $e, $timeout); if ($nb === 0) $nb = count($r); while (!@feof($this->_socket) && $nb !== false && $nb > 0) { $timeout = 0; if (count($r) > 0) { $res = @stream_socket_recvfrom($this->_socket, 8192); if ($res == '') { // should not happen habitually, but... break; } elseif ($res !== false) { $this->_response .= $res; } else { if (isset($this->_spool[0])) { $time = microtime(true); $this->_spool[0]['Times']['receive'][1] = $time - $this->_spool[0]['Times']['receive'][0]; } $this->_bad('Error(' . $errno . ') ' . $errstr . ', could not read all data!'); return; } } // if timeout then error if (($difftime = microtime(true) - $this->_read_time) > $waittimeout) { $this->_bad('Request timeout, in _receive (' . round($difftime) . ' > ' . $waittimeout . 's)'); break; } $r = array($this->_socket); $w = null; $e = null; $nb = @stream_select($r, $w, $e, $timeout); if ($nb === 0) $nb = count($r); } if (isset($this->_spool[0]['Times']['receive'][2])) { $time = microtime(true); $this->_spool[0]['Times']['receive'][2] += ($time - $time0); } // get headers and full message $state = $this->_handleHeaders(); //echo "receive9\n"; //var_dump($state); } while ($this->_wait && $state === false && $this->_socket && !@feof($this->_socket)); if (!isset($this->_spool[0]['State']) || $this->_spool[0]['State'] != 'RECEIVE') { // in case of (probably keepalive) connection closed by server if ($this->_socket && @feof($this->_socket)){ //$aseco->console($this->_webaccess_str . 'Socket closed by server (' . $this->_host . ')'); $this->_state = 'CLOSED'; @fclose($this->_socket); $this->_socket = null; } return; } // terminated but incomplete! more than probably closed by server... if ($state === false && $this->_socket && @feof($this->_socket)) { $this->_state = 'CLOSED'; if (isset($this->_spool[0])) { $time = microtime(true); $this->_spool[0]['State'] = 'OPEN'; $this->_spool[0]['Times']['receive'][1] = $time - $this->_spool[0]['Times']['receive'][0]; } if (strlen($this->_response) > 0) // if not 0 sized then show error message $this->_bad('Error: closed with incomplete read: re-open socket and re-send! (' . strlen($this->_response) . ')'); else $this->_bad('Closed by server when reading: re-open socket and re-send! (' . strlen($this->_response) . ')', false); $this->_spool[0]['Retries']++; if ($this->_spool[0]['Retries'] > 2) { // 3 tries failed, remove entry from spool $aseco->console($this->_webaccess_str . "failed {$this->_spool[0]['Retries']} times: skip current request"); array_shift($this->_spool); } return; } // reply is complete :) if ($state === true) { $this->_bad_timeout = 0; // reset error timeout $this->_spool[0]['Times']['receive'][1] = $time - $this->_spool[0]['Times']['receive'][0]; $this->_spool[0]['State'] = 'DONE'; // store http/xml response in global $_Webaccess_last_response for debugging use $_Webaccess_last_response = $this->_spool[0]['Response']; //debugPrint('Webaccess->_receive - Response', $_Webaccess_last_response); // call callback func $this->_callCallback(); $this->_query_time = time(); if (!$this->_keepalive || $this->_spool[0]['Close']) { //if ($this->_spool[0]['Close']) // $aseco->console($this->_webaccess_str . 'close connection (asked in headers)'); $this->_state = 'CLOSED'; @fclose($this->_socket); $this->_socket = null; } $this->infos(); // request completed, remove it from spool! array_shift($this->_spool); } } // _receive private function _callCallback($error = null) { // store optional error message if ($error !== null) $this->_spool[0]['Response']['Error'] = $error; // call callback func if (isset($this->_spool[0]['Callback'])) { $callbackinfo = $this->_spool[0]['Callback']; if (isset($callbackinfo[0]) && is_callable($callbackinfo[0])) { $callback_func = $callbackinfo[0]; $callbackinfo[0] = $this->_spool[0]['Response']; call_user_func_array($callback_func, $callbackinfo); } } } private function _handleHeaders() { global $aseco, $_wa_header_separator, $_wa_header_multi; if (!isset($this->_spool[0]['State'])) return false; if (strlen($this->_response) < 8) // not enough data, continue read return false; if (strncmp($this->_response, 'HTTP/', 5) != 0) { // not HTTP! $this->_bad("Error, not HTTP response ! **********\n" . substr($this->_response, 0, 300) . "\n***************\n"); return null; } // separate headers and data $datas = explode("\r\n\r\n", $this->_response, 2); if (count($datas) < 2) { $datas = explode("\n\n", $this->_response, 2); if (count($datas) < 2) { $datas = explode("\r\r", $this->_response, 2); if (count($datas) < 2) return false; // not complete headers, continue read } } // get headers if not done on previous read if (!isset($this->_spool[0]['Headers']['Command'][0])) { // separate headers //echo "Get Headers! (" . strlen($datas[0]) . ")\n"; $headers = array(); $heads = explode("\n", str_replace("\r", "\n", str_replace("\r\n", "\n", $datas[0]))); if (count($heads) < 2) { $this->_bad("Error, uncomplete headers! **********\n" . $datas[0] . "\n***************\n"); return null; } $headers['Command'] = explode(' ', $heads[0], 3); for ($i = 1; $i < count($heads); $i++) { $header = explode(':', $heads[$i], 2); if (count($header) > 1) { $headername = strtolower(trim($header[0])); if (isset($_wa_header_separator[$headername])) $sep = $_wa_header_separator[$headername]; else $sep = ','; if (isset($_wa_header_multi[$headername]) && $_wa_header_multi[$headername]) { if (!isset($headers[$headername])) $headers[$headername] = array(); $headers[$headername][] = explode($sep, trim($header[1])); } else $headers[$headername] = explode($sep, trim($header[1])); } } if (isset($headers['content-length'][0])) $headers['content-length'][0] += 0; // convert to int $this->_spool[0]['Headers'] = $headers; // add header specific info in case of Dedimania reply if (isset($headers['server'][0])) $this->_webaccess_str = 'Webaccess (' . $this->_host . ':' . $this->_port .'/'. $headers['server'][0] . '): '; } else { $headers = &$this->_spool[0]['Headers']; //echo "Previous Headers! (" . strlen($datas[0]) . ")\n"; } // get real message $datasize = strlen($datas[1]); if (isset($headers['content-length'][0]) && $headers['content-length'][0] >= 0) { //echo 'mess_size0=' . strlen($datas[1]) . "\n"; if ($headers['content-length'][0] > $datasize) // incomplete message return false; elseif ($headers['content-length'][0] < $datasize) { $message = substr($datas[1], 0, $headers['content-length'][0]); // remaining buffer for next reply $this->_response = substr($datas[1], $headers['content-length'][0]); } else { $message = $datas[1]; $this->_response = ''; } $this->_spool[0]['ResponseSize'] = strlen($datas[0]) + 4 + $headers['content-length'][0]; } // get real message when reply is chunked elseif (isset($headers['transfer-encoding'][0]) && $headers['transfer-encoding'][0] == 'chunked') { // get chunk size and make message with chunks data $size = -1; $chunkpos = 0; if (($datapos = strpos($datas[1], "\r\n", $chunkpos)) !== false) { $message = ''; $chunk = explode(';', substr($datas[1], $chunkpos, $datapos - $chunkpos)); $size = hexdec($chunk[0]); //debugPrint("Webaccess->Response - chunk - $chunkpos, $datapos, $size (" . strlen($datas[1]) . ")", $chunk); while ($size > 0) { if ($datapos + 2 + $size > $datasize) // incomplete message return false; $message .= substr($datas[1], $datapos + 2, $size); $chunkpos = $datapos + 2 + $size + 2; if (($datapos = strpos($datas[1], "\r\n", $chunkpos)) !== false) { $chunk = explode(';', substr($datas[1], $chunkpos, $datapos - $chunkpos)); $size = hexdec($chunk[0]); } else $size = -1; //debugPrint("Webaccess->Response - chunk - $chunkpos, $datapos, $size (" . strlen($datas[1]) . ")", $chunk); } } if ($size < 0) // error bad size or incomplete message return false; if (strpos($datas[1], "\r\n\r\n", $chunkpos) === false) // incomplete message: end is missing return false; // store complete message size $msize = strlen($message); $headers['transfer-encoding'][1] = 'total_size=' . $msize; // add message size after 'chunked' for information $this->_spool[0]['ResponseSize'] = strlen($datas[0]) + 4 + $msize; // after the message itself... $message_end = explode("\r\n\r\n", substr($datas[1], $chunkpos), 2); // add end headers if any $heads = explode("\n", str_replace("\r", "\n", str_replace("\r\n", "\n", $message_end[0]))); for ($i = 1; $i < count($heads); $i++) { $header = explode(':', $heads[$i], 2); if (count($header) > 1) { $headername = strtolower(trim($header[0])); if (isset($_wa_header_separator[$headername])) $sep = $_wa_header_separator[$headername]; else $sep = ','; if (isset($_wa_header_multi[$headername]) && $_wa_header_multi[$headername]) { if (!isset($headers[$headername])) $headers[$headername] = array(); $headers[$headername][] = explode($sep, trim($header[1])); } else $headers[$headername] = explode($sep, trim($header[1])); } } $this->_spool[0]['Headers'] = $headers; // remaining buffer for next reply if (isset($message_end[1]) && strlen($message_end[1]) > 0) { $this->_response = $message_end[1]; } else $this->_response = ''; } // no content-length and not chunked! else { $this->_bad("Error, bad http, no content-length and not chunked! **********\n" . $datas[0] . "\n***************\n"); return null; } //echo 'mess_size1=' . strlen($message) . "\n"; // if Content-Encoding: gzip or Content-Encoding: deflate if (isset($headers['content-encoding'][0])) { if ($headers['content-encoding'][0] == 'gzip') $message = @gzdecode($message); elseif ($headers['content-encoding'][0] == 'deflate') $message = @gzinflate($message); } // if Accept-Encoding: gzip or deflate if ($this->_compress_request == 'accept' && isset($headers['accept-encoding'][0])) { foreach ($headers['accept-encoding'] as $comp) { $comp = trim($comp); if ($comp == 'gzip' && function_exists('gzencode')) { $this->_compress_request = 'gzip'; break; } elseif ($comp == 'deflate' && function_exists('gzdeflate')) { $this->_compress_request = 'deflate'; break; } } if ($this->_compress_request == 'accept') $this->_compress_request = false; $aseco->console($this->_webaccess_str . 'send: ' . ($this->_compress_request === false ? 'no compression' : $this->_compress_request) . ', receive: ' . (isset($headers['content-encoding'][0]) ? $headers['content-encoding'][0] : 'no compression')); } // get cookies values if (isset($headers['set-cookie'])) { foreach ($headers['set-cookie'] as $cookie) { $cook = explode('=', $cookie[0], 2); if (count($cook) > 1) { // set main cookie value $cookname = trim($cook[0]); if (!isset($this->_cookies[$cookname])) $this->_cookies[$cookname] = array(); $this->_cookies[$cookname]['Value'] = trim($cook[1]); // set cookie options for ($i = 1; $i < count($cookie); $i++) { $cook = explode('=', $cookie[$i], 2); $cookarg = strtolower(trim($cook[0])); if (isset($cook[1])) $this->_cookies[$cookname][$cookarg] = trim($cook[1]); } } } //debugPrint('SET-COOKIES: ', $headers['set-cookie']); //debugPrint('STORED COOKIES: ', $this->_cookies); } // if the server reply ask to close, then close if (!isset($headers['connection'][0]) || $headers['connection'][0] == 'close') { //if (!$this->_spool[0]['Close']) // $aseco->console($this->_webaccess_str . 'server ask to close connection'); $this->_spool[0]['Close'] = true; } // verify server keepalive value and use them if lower if (isset($headers['keep-alive'])) { $kasize = count($headers['keep-alive']); for ($i = 0; $i < $kasize; $i++) { $keep = explode('=', $headers['keep-alive'][$i], 2); if (count($keep) > 1) $headers['keep-alive'][trim(strtolower($keep[0]))] = intval(trim($keep[1])); } if (isset($headers['keep-alive']['timeout'])) $this->_serv_keepalive_timeout = $headers['keep-alive']['timeout']; if (isset($headers['keep-alive']['max'])) $this->_serv_keepalive_max = $headers['keep-alive']['max']; //$aseco->console($this->_webaccess_str . 'max=' . $this->_serv_keepalive_max . ', timeout=' . $this->_serv_keepalive_timeout . "\n"); } // store complete reply message for the request $this->_spool[0]['Response'] = array('Code' => intval($headers['Command'][1]), 'Reason' => $headers['Command'][2], 'Headers' => $headers, 'Message' => $message ); //echo 'mess_size2=' . strlen($message) . "\n"; return true; } // _handleHeaders function infos() { global $aseco; $size = (isset($this->_spool[0]['Response']['Message'])) ? strlen($this->_spool[0]['Response']['Message']) : 0; $msg = $this->_webaccess_str . sprintf('[%s,%s]: %0.3f / %0.3f / %0.3f (%0.3f) / %d [%d,%d,%d]', $this->_state, $this->_spool[0]['State'], $this->_spool[0]['Times']['open'][1], $this->_spool[0]['Times']['send'][1], $this->_spool[0]['Times']['receive'][1], $this->_spool[0]['Times']['receive'][2], $this->_query_num, $this->_spool[0]['DatasSize'], $size, $this->_spool[0]['ResponseSize']); //$aseco->console($msg); } // infos } // class WebaccessUrl // use: list($host, $port, $path) = getHostPortPath($url); function getHostPortPath($url) { $http_pos = strpos($url, 'http://'); if ($http_pos !== false) { $script = explode('/', substr($url, $http_pos + 7), 2); if (isset($script[1])) $path = '/' . $script[1]; else $path = '/'; $serv = explode(':', $script[0], 2); $host = $serv[0]; if (isset($serv[1])) $port = (int)$serv[1]; else $port = 80; if (strlen($host) > 2) return array($host, $port, $path); } return array(false, false, false); } // getHostPortPath // gzdecode() workaround if (!function_exists('gzdecode') && function_exists('gzinflate')) { function gzdecode($data) { $len = strlen($data); if ($len < 18 || strcmp(substr($data, 0, 2), "\x1f\x8b")) { return null; // Not GZIP format (See RFC 1952) } $method = ord(substr($data, 2, 1)); // Compression method $flags = ord(substr($data, 3, 1)); // Flags if ($flags & 31 != $flags) { // Reserved bits are set -- NOT ALLOWED by RFC 1952 return null; } // NOTE: $mtime may be negative (PHP integer limitations) $mtime = unpack('V', substr($data, 4, 4)); $mtime = $mtime[1]; $xfl = substr($data, 8, 1); $os = substr($data, 8, 1); $headerlen = 10; $extralen = 0; $extra = ''; if ($flags & 4) { // 2-byte length prefixed EXTRA data in header if ($len - $headerlen - 2 < 8) { return false; // Invalid format } $extralen = unpack('v', substr($data, 8, 2)); $extralen = $extralen[1]; if ($len - $headerlen - 2 - $extralen < 8) { return false; // Invalid format } $extra = substr($data, 10, $extralen); $headerlen += $extralen + 2; } $filenamelen = 0; $filename = ''; if ($flags & 8) { // C-style string file NAME data in header if ($len - $headerlen - 1 < 8) { return false; // Invalid format } $filenamelen = strpos(substr($data, 8 + $extralen), chr(0)); if ($filenamelen === false || $len - $headerlen - $filenamelen - 1 < 8) { return false; // Invalid format } $filename = substr($data, $headerlen, $filenamelen); $headerlen += $filenamelen + 1; } $commentlen = 0; $comment = ''; if ($flags & 16) { // C-style string COMMENT data in header if ($len - $headerlen - 1 < 8) { return false; // Invalid format } $commentlen = strpos(substr($data, 8 + $extralen + $filenamelen), chr(0)); if ($commentlen === false || $len - $headerlen - $commentlen - 1 < 8) { return false; // Invalid header format } $comment = substr($data, $headerlen, $commentlen); $headerlen += $commentlen + 1; } $headercrc = ''; if ($flags & 1) { // 2-bytes (lowest order) of CRC32 on header present if ($len - $headerlen - 2 < 8) { return false; // Invalid format } $calccrc = crc32(substr($data, 0, $headerlen)) & 0xffff; $headercrc = unpack('v', substr($data, $headerlen, 2)); $headercrc = $headercrc[1]; if ($headercrc != $calccrc) { return false; // Bad header CRC } $headerlen += 2; } // GZIP FOOTER - These be negative due to PHP's limitations $datacrc = unpack('V', substr($data, -8, 4)); $datacrc = $datacrc[1]; $isize = unpack('V', substr($data, -4)); $isize = $isize[1]; // Perform the decompression: $bodylen = $len - $headerlen - 8; if ($bodylen < 1) { // This should never happen - IMPLEMENTATION BUG! return null; } $body = substr($data, $headerlen, $bodylen); $data = ''; if ($bodylen > 0) { switch ($method) { case 8: // Currently the only supported compression method: $data = gzinflate($body); break; default: // Unknown compression method return false; } } else { // I'm not sure if zero-byte body content is allowed. // Allow it for now... Do nothing... } // Verify decompressed size and CRC32: // NOTE: This may fail with large data sizes depending on how // PHP's integer limitations affect strlen() since $isize // may be negative for large sizes if ($isize != strlen($data) || crc32($data) != $datacrc) { // Bad format! Length or CRC doesn't match! return false; } return $data; } // gzdecode } ?>