docker-tmserver/xaseco/includes/web_access.inc.php

1342 lines
46 KiB
PHP
Raw Permalink Normal View History

2022-06-26 17:43:44 +02:00
<?php
/* vim: set noexpandtab tabstop=2 softtabstop=2 shiftwidth=2: */
////////////////////////////////////////////////////////////////
//
// File: WEB ACCESS 2.1.3
// Date: 13.10.2011
// Author: Gilles Masson
// Updated: Xymph
//
////////////////////////////////////////////////////////////////
// This class and functions can be used to make asynchronous xml or http
// (POST or GET) queries.
// This means that you call a function to send the query, and a callback
// function will automatically be called when the response has arrived,
// without having your program waiting for the response.
// You can also use it for synchronous queries (see below).
// The class handles (for each URL) keepalive and compression (when possible).
// It supports Cookies, and so can use sessions like php one (anyway the cookie
// is not stored, so its maximal life is the life of the program).
//
//
// usage: $_webaccess = new Webaccess();
// $_webaccess->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
}
?>