class_HTTPRetriever.php
上传用户:jamesxinda
上传日期:2022-07-31
资源大小:28k
文件大小:39k
源码类别:

WEB源码(ASP,PHP,...)

开发平台:

PHP

  1. <?php
  2. /* HTTP Retriever
  3.  * Version v1.1.10
  4.  * Copyright 2004-2007, Steve Blinch
  5.  * http://code.blitzaffe.com
  6.  * ============================================================================
  7.  *
  8.  * DESCRIPTION
  9.  *
  10.  * Provides a pure-PHP implementation of an HTTP v1.1 client, including support
  11.  * for chunked transfer encoding and user agent spoofing.  Both GET and POST
  12.  * requests are supported.
  13.  *
  14.  * This can be used in place of something like CURL or WGET for HTTP requests.
  15.  * Native SSL (HTTPS) requests are also supported if the OpenSSL extension is 
  16.  * installed under PHP v4.3.0 or greater.
  17.  *
  18.  * If native SSL support is not available, the class will also check for the
  19.  * CURL extension; if it's installed, it will transparently be used for SSL
  20.  * (HTTPS) requests.
  21.  *
  22.  * If neither native SSL support nor the CURL extension are available, and
  23.  * libcurlemu (a CURL emulation library available from our web site) is found,
  24.  * the class will also check for the CURL console binary (usually in 
  25.  * /usr/bin/curl); if it's installed, it will transparently be used for SSL
  26.  * requests.
  27.  *
  28.  * In short, if it's possible to make an HTTP/HTTPS request from your server,
  29.  * this class can most likely do it.
  30.  *
  31.  *
  32.  * HISTORY
  33.  *
  34.  * 1.1.10 (13-Feb-2006)
  35.  * - Fixed bug wherein libcurlemu may not be correctly included when
  36.  *   needed.
  37.  * - Fixed bug wherein stream read timeouts may not be recognized
  38.  * - Adjusted timeout handling code to better handle timeout conditions
  39.  * - Added intelligent caching support
  40.  * - Caching is now better-handled for high-volume requests
  41.  * - Added postprocessing callback support
  42.  * - Improved redirect support
  43.  * - Fixed bug in which POST requests couldn't use GET-style query strings
  44.  * - Added header cleanup between requests
  45.  * - Added partial proxy support via $http->curl_proxy (only useable when
  46.  *   $http->force_curl is TRUE; internal support not yet implemented)
  47.  *
  48.  *
  49.  * 1.1.9 (11-Oct-2006)
  50.  * - Added set_transfer_display() and default_transfer_callback()
  51.  *   methods for transfer progress tracking
  52.  * - Suppressed possible "fatal protocol error" when remote SSL server
  53.  *   closes the connection early
  54.  * - Added get_content_type() method
  55.  * - make_query_string() now handles arrays
  56.  *
  57.  * 1.1.8 (19-Jun-2006)
  58.  * - Added set_progress_display() and default_progress_callback()
  59.  *   methods for debug output
  60.  * - Added support for relative URLs in HTTP redirects
  61.  * - Added cookie support (sending and receiving)
  62.  * - Numerous bug fixes
  63.  *
  64.  * 1.1.7 (18-Apr-2006)
  65.  * - Added support for automatically following HTTP redirects
  66.  * - Added ::get_error() method to get any available error message (be
  67.  *   it an HTTP result error or an internal/connection error)
  68.  * - Added ::cache_hit variable to determine whether the page was cached
  69.  *
  70.  * 1.1.6 (04-Mar-2006)
  71.  * - Added stream_timeout class variable.
  72.  * - Added progress_callback class variable.
  73.  * - Added support for braindead servers that ignore Connection: close
  74.  *
  75.  *
  76.  * EXAMPLE
  77.  *
  78.  * // HTTPRetriever usage example
  79.  * require_once("class_HTTPRetriever.php");
  80.  * $http = &new HTTPRetriever();
  81.  *
  82.  *
  83.  * // Example GET request:
  84.  * // ----------------------------------------------------------------------------
  85.  * $keyword = "blitzaffe code"; // search Google for this keyword
  86.  * if (!$http->get("http://www.google.com/search?hl=en&q=%22".urlencode($keyword)."%22&btnG=Search&meta=")) {
  87.  *     echo "HTTP request error: #{$http->result_code}: {$http->result_text}";
  88.  *     return false;
  89.  * }
  90.  * echo "HTTP response headers:<br><pre>";
  91.  * var_dump($http->response_headers);
  92.  * echo "</pre><br>";
  93.  * 
  94.  * echo "Page content:<br><pre>";
  95.  * echo $http->response;
  96.  * echo "</pre>";
  97.  * // ----------------------------------------------------------------------------
  98.  *  
  99.  *
  100.  * // Example POST request:
  101.  * // ----------------------------------------------------------------------------
  102.  * $keyword = "blitzaffe code"; // search Google for this keyword
  103.  * $values = array(
  104.  *     "hl"=>"en",
  105.  *     "q"=>"%22".urlencode($keyword)."%22",
  106.  *     "btnG"=>"Search",
  107.  *     "meta"=>""
  108.  * );
  109.  * // Note: This example is just to demonstrate the POST equivalent of the GET
  110.  * // example above; running this script will return a 501 Not Implemented, as
  111.  * // Google does not support POST requests.
  112.  * if (!$http->post("http://www.google.com/search",$http->make_query_string($values))) {
  113.  *     echo "HTTP request error: #{$http->result_code}: {$http->result_text}";
  114.  *     return false;
  115.  * }
  116.  * echo "HTTP response headers:<br><pre>";
  117.  * var_dump($http->response_headers);
  118.  * echo "</pre><br>";
  119.  * 
  120.  * echo "Page content:<br><pre>";
  121.  * echo $http->response;
  122.  * echo "</pre>";
  123.  * // ----------------------------------------------------------------------------
  124.  *
  125.  *
  126.  * LICENSE
  127.  *
  128.  * This script is free software; you can redistribute it and/or modify it under the
  129.  * terms of the GNU General Public License as published by the Free Software
  130.  * Foundation; either version 2 of the License, or (at your option) any later
  131.  * version.
  132.  *
  133.  * This script is distributed in the hope that it will be useful, but WITHOUT ANY
  134.  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  135.  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
  136.  * details.
  137.  *
  138.  * You should have received a copy of the GNU General Public License along
  139.  * with this script; if not, write to the Free Software Foundation, Inc.,
  140.  * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  141.  */
  142. // define user agent ID's
  143. define("UA_EXPLORER",0);
  144. define("UA_MOZILLA",1);
  145. define("UA_FIREFOX",2);
  146. define("UA_OPERA",3);
  147. // define progress message severity levels
  148. define('HRP_DEBUG',0);
  149. define('HRP_INFO',1);
  150. define('HRP_ERROR',2);
  151. if (!defined("CURL_PATH")) define("CURL_PATH","/usr/bin/curl");
  152. // if the CURL extension is not loaded, but the CURL Emulation Library is found, try
  153. // to load it
  154. if (!extension_loaded("curl") && !defined('HTTPR_NO_REDECLARE_CURL') ) {
  155. foreach (array(dirname(__FILE__)."/",dirname(__FILE__)."/libcurlemu/") as $k=>$libcurlemupath) {
  156. $libcurlemuinc = $libcurlemupath.'libcurlemu.inc.php';
  157. if (is_readable($libcurlemuinc)) require_once($libcurlemuinc);
  158. }
  159. }
  160. class HTTPRetriever {
  161. // Constructor
  162. function HTTPRetriever() {
  163. // default HTTP headers to send with all requests
  164. $this->headers = array(
  165. "Referer"=>"",
  166. "User-Agent"=>"HTTPRetriever/1.0",
  167. "Connection"=>"close"
  168. );
  169. // HTTP version (has no effect if using CURL)
  170. $this->version = "1.1";
  171. // Normally, CURL is only used for HTTPS requests; setting this to
  172. // TRUE will force CURL for HTTP requests as well.  Not recommended.
  173. $this->force_curl = false;
  174. // If you don't want to use CURL at all, set this to TRUE.
  175. $this->disable_curl = false;
  176. // If HTTPS request return an error message about SSL certificates in
  177. // $this->error and you don't care about security, set this to TRUE
  178. $this->insecure_ssl = false;
  179. // Set the maximum time to wait for a connection
  180. $this->connect_timeout = 15;
  181. // Set the maximum time to allow a transfer to run, or 0 to disable.
  182. $this->max_time = 0;
  183. // Set the maximum time for a socket read/write operation, or 0 to disable.
  184. $this->stream_timeout = 0;
  185. // If you're making an HTTPS request to a host whose SSL certificate
  186. // doesn't match its domain name, AND YOU FULLY UNDERSTAND THE
  187. // SECURITY IMPLICATIONS OF IGNORING THIS PROBLEM, set this to TRUE.
  188. $this->ignore_ssl_hostname = false;
  189. // If TRUE, the get() and post() methods will close the connection
  190. // and return immediately after receiving the HTTP result code
  191. $this->result_close = false;
  192. // If set to a positive integer value, retrieved pages will be cached
  193. // for this number of seconds.  Any subsequent calls within the cache
  194. // period will return the cached page, without contacting the remote
  195. // server.
  196. $this->caching = false;
  197. // If TRUE and $this->caching is not false, retrieved pages/files will be
  198. // cached only if they appear to be static.
  199. $this->caching_intelligent = false;
  200. // If TRUE, cached files will be stored in subdirectories corresponding
  201. // to the first 2 letters of the hash filename
  202. $this->caching_highvolume = false;
  203. // If $this->caching is enabled, this specifies the folder under which
  204. // cached pages are saved.
  205. $this->cache_path = '/tmp/';
  206. // Set these to perform basic HTTP authentication
  207. $this->auth_username = '';
  208. $this->auth_password = '';
  209. // Optionally set this to a valid callback method to have HTTPRetriever
  210. // provide page preprocessing capabilities to your script.  If set, this
  211. // method should accept two arguments: an object representing an instance
  212. // of HTTPRetriever, and a string containing the page contents
  213. $this->page_preprocessor = null;
  214. // Optionally set this to a valid callback method to have HTTPRetriever
  215. // provide progress messages.  Your callback must accept 2 parameters:
  216. // an integer representing the severity (0=debug, 1=information, 2=error),
  217. // and a string representing the progress message
  218. $this->progress_callback = null;
  219. // Optionally set this to a valid callback method to have HTTPRetriever
  220. // provide bytes-transferred messages.  Your callbcak must accept 2
  221. // parameters: an integer representing the number of bytes transferred,
  222. // and an integer representing the total number of bytes expected (or
  223. // -1 if unknown).
  224. $this->transfer_callback = null;
  225. // Set this to TRUE if you HTTPRetriever to transparently follow HTTP
  226. // redirects (code 301, 302, 303, and 307).  Optionally set this to a
  227. // numeric value to limit the maximum number of redirects to the specified
  228. // value.  (Redirection loops are detected automatically.)
  229. // Note that non-GET/HEAD requests will NOT be redirected except on code
  230. // 303, as per HTTP standards.
  231. $this->follow_redirects = false;
  232. }
  233. // Send an HTTP GET request to $url; if $ipaddress is specified, the
  234. // connection will be made to the selected IP instead of resolving the 
  235. // hostname in $url.
  236. //
  237. // If $cookies is set, it should be an array in one of two formats.
  238. //
  239. // Either: $cookies[ 'cookiename' ] = array (
  240. // '/path/'=>array(
  241. // 'expires'=>time(),
  242. // 'domain'=>'yourdomain.com',
  243. // 'value'=>'cookievalue'
  244. // )
  245. // );
  246. //
  247. // Or, a more simplified format:
  248. // $cookies[ 'cookiename' ] = 'value';
  249. //
  250. // The former format will automatically check to make sure that the path, domain,
  251. // and expiration values match the HTTP request, and will only send the cookie if
  252. // they do match.  The latter will force the cookie to be set for the HTTP request
  253. // unconditionally.
  254. // 
  255. function get($url,$ipaddress = false,$cookies = false) {
  256. $this->method = "GET";
  257. $this->post_data = "";
  258. $this->connect_ip = $ipaddress;
  259. return $this->_execute_request($url,$cookies);
  260. }
  261. // Send an HTTP POST request to $url containing the POST data $data.  See ::get()
  262. // for a description of the remaining arguments.
  263. function post($url,$data="",$ipaddress = false,$cookies = false) {
  264. $this->method = "POST";
  265. $this->post_data = $data;
  266. $this->connect_ip = $ipaddress;
  267. return $this->_execute_request($url,$cookies);
  268. }
  269. // Send an HTTP HEAD request to $url.  See ::get() for a description of the arguments.
  270. function head($url,$ipaddress = false,$cookies = false) {
  271. $this->method = "HEAD";
  272. $this->post_data = "";
  273. $this->connect_ip = $ipaddress;
  274. return $this->_execute_request($url,$cookies);
  275. }
  276. // send an alternate (non-GET/POST) HTTP request to $url
  277. function custom($method,$url,$data="",$ipaddress = false,$cookies = false) {
  278. $this->method = $method;
  279. $this->post_data = $data;
  280. $this->connect_ip = $ipaddress;
  281. return $this->_execute_request($url,$cookies);
  282. }
  283. function array_to_query($arrayname,$arraycontents) {
  284. $output = "";
  285. foreach ($arraycontents as $key=>$value) {
  286. if (is_array($value)) {
  287. $output .= $this->array_to_query(sprintf('%s[%s]',$arrayname,urlencode($key)),$value);
  288. } else {
  289. $output .= sprintf('%s[%s]=%s&',$arrayname,urlencode($key),urlencode($value));
  290. }
  291. }
  292. return $output;
  293. }
  294. // builds a query string from the associative array array $data;
  295. // returns a string that can be passed to $this->post()
  296. function make_query_string($data) {
  297. $output = "";
  298. if (is_array($data)) {
  299. foreach ($data as $name=>$value) {
  300. if (is_array($value)) {
  301. $output .= $this->array_to_query(urlencode($name),$value);
  302. } elseif (is_scalar($value)) {
  303. $output .= urlencode($name)."=".urlencode($value)."&";
  304. } else {
  305. $output .= urlencode($name)."=".urlencode(serialize($value)).'&';
  306. }
  307. }
  308. }
  309. return substr($output,0,strlen($output)-1);
  310. }
  311. // this is pretty limited... but really, if you're going to spoof you UA, you'll probably
  312. // want to use a Windows OS for the spoof anyway
  313. //
  314. // if you want to set the user agent to a custom string, just assign your string to
  315. // $this->headers["User-Agent"] directly
  316. function set_user_agent($agenttype,$agentversion,$windowsversion) {
  317. $useragents = array(
  318. "Mozilla/4.0 (compatible; MSIE %agent%; Windows NT %os%)", // IE
  319. "Mozilla/5.0 (Windows; U; Windows NT %os%; en-US; rv:%agent%) Gecko/20040514", // Moz
  320. "Mozilla/5.0 (Windows; U; Windows NT %os%; en-US; rv:1.7) Gecko/20040803 Firefox/%agent%", // FFox
  321. "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT %os%) Opera %agent%  [en]", // Opera
  322. );
  323. $agent = $useragents[$agenttype];
  324. $this->headers["User-Agent"] = str_replace(array("%agent%","%os%"),array($agentversion,$windowsversion),$agent);
  325. }
  326. // this isn't presently used as it's now handled inline by the request parser
  327. function remove_chunkiness() {
  328. $remaining = $this->response;
  329. $this->response = "";
  330. while ($remaining) {
  331. $hexlen = strpos($remaining,"r");
  332. $chunksize = substr($remaining,0,$hexlen);
  333. $argstart = strpos($chunksize,';');
  334. if ($argstart!==false) $chunksize = substr($chunksize,0,$argstart);
  335. $chunksize = (int) @hexdec($chunksize);
  336. $this->response .= substr($remaining,$hexlen+2,$chunksize);
  337. $remaining = substr($remaining,$hexlen+2+$chunksize+2);
  338. if (!$chunksize) {
  339. // either we're done, or something's borked... exit
  340. $this->response .= $remaining;
  341. return;
  342. }
  343. }
  344. }
  345. // (internal) store a page in the cache
  346. function _cache_store($token,$url) {
  347. if ($this->caching_intelligent) {
  348. $urlinfo = parse_url($url);
  349. if ($this->method=='POST') {
  350. $this->progress(HRP_DEBUG,"POST request; not caching");
  351. return;
  352. } else if (strlen($urlinfo['query'])) {
  353. $this->progress(HRP_DEBUG,"Request used query string; not caching");
  354. return;
  355. } else {
  356. $this->progress(HRP_DEBUG,"Request appears to be static and cacheable");
  357. }
  358. }
  359. $values = array(
  360. "stats"=>$this->stats,
  361. "result_code"=>$this->result_code,
  362. "result_text"=>$this->result_text,
  363. "version"=>$this->version,
  364. "response"=>$this->response,
  365. "response_headers"=>$this->response_headers,
  366. "response_cookies"=>$this->response_cookies,
  367. "raw_response"=>$this->raw_response,
  368. );
  369. $values = serialize($values);
  370. $cache_dir = $this->cache_path;
  371. if (substr($cache_dir,-1)!='/') $cache_dir .= '/';
  372. if ($this->caching_highvolume) {
  373. $cache_dir .= substr($token,0,2) . '/';
  374. if (!is_dir($cache_dir)) @mkdir($cache_dir);
  375. }
  376. $filename = $cache_dir.$token.'.tmp';
  377. $fp = @fopen($filename,"w");
  378. if (!$fp) {
  379. $this->progress(HRP_DEBUG,"Unable to create cache file");
  380. return false;
  381. }
  382. fwrite($fp,$values);
  383. fclose($fp);
  384. $this->progress(HRP_DEBUG,"HTTP response stored to cache");
  385. }
  386. // (internal) fetch a page from the cache
  387. function _cache_fetch($token) {
  388. $this->cache_hit = false;
  389. $this->progress(HRP_DEBUG,"Checking for cached page value");
  390. $cache_dir = $this->cache_path;
  391. if (substr($cache_dir,-1)!='/') $cache_dir .= '/';
  392. if ($this->caching_highvolume) $cache_dir .= substr($token,0,2) . '/';
  393. $filename = $cache_dir.$token.'.tmp';
  394. if (!file_exists($filename)) {
  395. $this->progress(HRP_DEBUG,"Page not available in cache");
  396. return false;
  397. }
  398. if (time()-filemtime($filename)>$this->caching) {
  399. $this->progress(HRP_DEBUG,"Page in cache is expired");
  400. @unlink($filename);
  401. return false;
  402. }
  403. if ($values = file_get_contents($filename)) {
  404. $values = unserialize($values);
  405. if (!$values) {
  406. $this->progress(HRP_DEBUG,"Invalid cache contents");
  407. return false;
  408. }
  409. $this->stats = $values["stats"];
  410. $this->result_code = $values["result_code"];
  411. $this->result_text = $values["result_text"];
  412. $this->version = $values["version"];
  413. $this->response = $values["response"];
  414. $this->response_headers = $values["response_headers"];
  415. $this->response_cookies = $values["response_cookies"];
  416. $this->raw_response = $values["raw_response"];
  417. $this->progress(HRP_DEBUG,"Page loaded from cache");
  418. $this->cache_hit = true;
  419. return true;
  420. } else {
  421. $this->progress(HRP_DEBUG,"Error reading cache file");
  422. return false;
  423. }
  424. }
  425. function parent_path($path) {
  426. if (substr($path,0,1)=='/') $path = substr($path,1);
  427. if (substr($path,-1)=='/') $path = substr($path,0,strlen($path)-1);
  428. $path = explode('/',$path);
  429. array_pop($path);
  430. return count($path) ? ('/' . implode('/',$path)) : '';
  431. }
  432. // $cookies should be an array in one of two formats.
  433. //
  434. // Either: $cookies[ 'cookiename' ] = array (
  435. // '/path/'=>array(
  436. // 'expires'=>time(),
  437. // 'domain'=>'yourdomain.com',
  438. // 'value'=>'cookievalue'
  439. // )
  440. // );
  441. //
  442. // Or, a more simplified format:
  443. // $cookies[ 'cookiename' ] = 'value';
  444. //
  445. // The former format will automatically check to make sure that the path, domain,
  446. // and expiration values match the HTTP request, and will only send the cookie if
  447. // they do match.  The latter will force the cookie to be set for the HTTP request
  448. // unconditionally.
  449. // 
  450. function response_to_request_cookies($cookies,$urlinfo) {
  451. // check for simplified cookie format (name=value)
  452. $cookiekeys = array_keys($cookies);
  453. if (!count($cookiekeys)) return;
  454. $testkey = array_pop($cookiekeys);
  455. if (!is_array($cookies[ $testkey ])) {
  456. foreach ($cookies as $k=>$v) $this->request_cookies[$k] = $v;
  457. return;
  458. }
  459. // must not be simplified format, so parse as complex format:
  460. foreach ($cookies as $name=>$paths) {
  461. foreach ($paths as $path=>$values) {
  462. // make sure the cookie isn't expired
  463. if ( isset($values['expires']) && ($values['expires']<time()) ) continue;
  464. $cookiehost = $values['domain'];
  465. $requesthost = $urlinfo['host'];
  466. // make sure the cookie is valid for this host
  467. $domain_match = (
  468. ($requesthost==$cookiehost) ||
  469. (substr($requesthost,-(strlen($cookiehost)+1))=='.'.$cookiehost)
  470. );
  471. // make sure the cookie is valid for this path
  472. $cookiepath = $path; if (substr($cookiepath,-1)!='/') $cookiepath .= '/';
  473. $requestpath = $urlinfo['path']; if (substr($requestpath,-1)!='/') $requestpath .= '/';
  474. if (substr($requestpath,0,strlen($cookiepath))!=$cookiepath) continue;
  475. $this->request_cookies[$name] = $values['value'];
  476. }
  477. }
  478. }
  479. // Execute the request for a particular URL, and transparently follow
  480. // HTTP redirects if enabled.  If $cookies is specified, it is assumed
  481. // to be an array received from $this->response_cookies and will be
  482. // processed to determine which cookies are valid for this host/URL.
  483. function _execute_request($url,$cookies = false) {
  484. // valid codes for which we transparently follow a redirect
  485. $redirect_codes = array(301,302,303,307);
  486. // valid methods for which we transparently follow a redirect
  487. $redirect_methods = array('GET','HEAD');
  488. $request_result = false;
  489. $this->followed_redirect = false;
  490. $this->response_cookies = array();
  491. $this->cookie_headers = '';
  492. $previous_redirects = array();
  493. do {
  494. // send the request
  495. $request_result = $this->_send_request($url,$cookies);
  496. $lasturl = $url;
  497. $url = false;
  498. // see if a redirect code was received
  499. if ($this->follow_redirects && in_array($this->result_code,$redirect_codes)) {
  500. // only redirect on a code 303 or if the method was GET/HEAD
  501. if ( ($this->result_code==303) || in_array($this->method,$redirect_methods) ) {
  502. // parse the information from the OLD URL so that we can handle
  503. // relative links
  504. $oldurlinfo = parse_url($lasturl);
  505. $url = $this->response_headers['Location'];
  506. // parse the information in the new URL, and fill in any blanks
  507. // using values from the old URL
  508. $urlinfo = parse_url($url);
  509. foreach ($oldurlinfo as $k=>$v) {
  510. if (!$urlinfo[$k]) $urlinfo[$k] = $v;
  511. }
  512. // create an absolute path
  513. if (substr($urlinfo['path'],0,1)!='/') {
  514. $baseurl = $oldurlinfo['path'];
  515. if (substr($baseurl,-1)!='/') $baseurl = $this->parent_path($url) . '/';
  516. $urlinfo['path'] = $baseurl . $urlinfo['path'];
  517. }
  518. // rebuild the URL
  519. $url = $this->rebuild_url($urlinfo);
  520. $this->method = "GET";
  521. $this->post_data = "";
  522. $this->progress(HRP_INFO,'Redirected to '.$url);
  523. }
  524. }
  525. if ( $url && strlen($url) ) {
  526. if (isset($previous_redirects[$url])) {
  527. $this->error = "Infinite redirection loop";
  528. $request_result = false;
  529. break;
  530. }
  531. if ( is_numeric($this->follow_redirects) && (count($previous_redirects)>$this->follow_redirects) ) {
  532. $this->error = "Exceeded redirection limit";
  533. $request_result = false;
  534. break;
  535. }
  536. $previous_redirects[$url] = true;
  537. }
  538. } while ($url && strlen($url));
  539. // clear headers that shouldn't persist across multiple requests
  540. $per_request_headers = array('Host','Content-Length');
  541. foreach ($per_request_headers as $k=>$v) unset($this->headers[$v]);
  542. if (count($previous_redirects)>1) $this->followed_redirect = array_keys($previous_redirects);
  543. return $request_result;
  544. }
  545. // private - sends an HTTP request to $url
  546. function _send_request($url,$cookies = false) {
  547. $this->progress(HRP_INFO,"Initiating {$this->method} request for $url");
  548. if ($this->caching) {
  549. $cachetoken = md5($url.'|'.$this->post_data);
  550. if ($this->_cache_fetch($cachetoken)) return true;
  551. }
  552. $time_request_start = $this->getmicrotime();
  553. $urldata = parse_url($url);
  554. $this->urldata = &$urldata;
  555. $http_host = $urldata['host'] . (isset($urldata['port']) ? ':'.$urldata['port'] : '');
  556. if (!isset($urldata["port"]) || !$urldata["port"]) $urldata["port"] = ($urldata["scheme"]=="https") ? 443 : 80;
  557. if (!isset($urldata["path"]) || !$urldata["path"]) $urldata["path"] = '/';
  558. if (!empty($urldata['user'])) $this->auth_username = $urldata['user'];
  559. if (!empty($urldata['pass'])) $this->auth_password = $urldata['pass'];
  560. //echo "Sending HTTP/{$this->version} {$this->method} request for ".$urldata["host"].":".$urldata["port"]." page ".$urldata["path"]."<br>";
  561. if ($this->version>"1.0") $this->headers["Host"] = $http_host;
  562. if ($this->method=="POST") {
  563. $this->headers["Content-Length"] = strlen($this->post_data);
  564. if (!isset($this->headers["Content-Type"])) $this->headers["Content-Type"] = "application/x-www-form-urlencoded";
  565. }
  566. if ( !empty($this->auth_username) || !empty($this->auth_password) ) {
  567. $this->headers['Authorization'] = 'Basic '.base64_encode($this->auth_username.':'.$this->auth_password);
  568. } else {
  569. unset($this->headers['Authorization']);
  570. }
  571. if (is_array($cookies)) {
  572. $this->response_to_request_cookies($cookies,$urldata);
  573. }
  574. if (!empty($urldata["query"])) $urldata["path"] .= "?".$urldata["query"];
  575. $request = $this->method." ".$urldata["path"]." HTTP/".$this->version."rn";
  576. $request .= $this->build_headers();
  577. $request .= $this->post_data;
  578. $this->response = "";
  579. // clear headers that shouldn't persist across multiple requests
  580. // (we can do this here as we've already built the request, including headers, above)
  581. $per_request_headers = array('Host','Content-Length');
  582. foreach ($per_request_headers as $k=>$v) unset($this->headers[$v]);
  583. // Native SSL support requires the OpenSSL extension, and was introduced in PHP 4.3.0
  584. $php_ssl_support = extension_loaded("openssl") && version_compare(phpversion(),"4.3.0")>=0;
  585. // if this is a plain HTTP request, or if it's an HTTPS request and OpenSSL support is available,
  586. // natively perform the HTTP request
  587. if ( ( ($urldata["scheme"]=="http") || ($php_ssl_support && ($urldata["scheme"]=="https")) ) && (!$this->force_curl) ) {
  588. $curl_mode = false;
  589. $hostname = $this->connect_ip ? $this->connect_ip : $urldata['host'];
  590. if ($urldata["scheme"]=="https") $hostname = 'ssl://'.$hostname;
  591. $time_connect_start = $this->getmicrotime();
  592. $this->progress(HRP_INFO,'Opening socket connection to '.$hostname.' port '.$urldata['port']);
  593. $this->expected_bytes = -1;
  594. $this->received_bytes = 0;
  595. $fp = @fsockopen ($hostname,$urldata["port"],$errno,$errstr,$this->connect_timeout);
  596. $time_connected = $this->getmicrotime();
  597. $connect_time = $time_connected - $time_connect_start;
  598. if ($fp) {
  599. if ($this->stream_timeout) stream_set_timeout($fp,$this->stream_timeout);
  600. $this->progress(HRP_INFO,"Connected; sending request");
  601. $this->progress(HRP_DEBUG,$request);
  602. fputs ($fp, $request);
  603. $this->raw_request = $request;
  604. if ($this->stream_timeout) {
  605. $meta = socket_get_status($fp);
  606. if ($meta['timed_out']) {
  607. $this->error = "Exceeded socket write timeout of ".$this->stream_timeout." seconds";
  608. $this->progress(HRP_ERROR,$this->error);
  609. return false;
  610. }
  611. }
  612. $this->progress(HRP_INFO,"Request sent; awaiting reply");
  613. $headers_received = false;
  614. $data_length = false;
  615. $chunked = false;
  616. $iterations = 0;
  617. while (!feof($fp)) {
  618. if ($data_length>0) {
  619. $line = fread($fp,$data_length);
  620. $this->progress(HRP_DEBUG,"[DL] Got a line: [{$line}] " . gettype($line));
  621. if ($line!==false) $data_length -= strlen($line);
  622. } else {
  623. $line = @fgets($fp,10240);
  624. $this->progress(HRP_DEBUG,"[NDL] Got a line: [{$line}] " . gettype($line));
  625. if ( ($chunked) && ($line!==false) ) {
  626. $line = trim($line);
  627. if (!strlen($line)) continue;
  628. list($data_length,) = explode(';',$line,2);
  629. $data_length = (int) hexdec(trim($data_length));
  630. if ($data_length==0) {
  631. $this->progress(HRP_DEBUG,"Done");
  632. // end of chunked data
  633. break;
  634. }
  635. $this->progress(HRP_DEBUG,"Chunk length $data_length (0x$line)");
  636. continue;
  637. }
  638. }
  639. if ($line===false) {
  640. $meta = socket_get_status($fp);
  641. if ($meta['timed_out']) {
  642. if ($this->stream_timeout) {
  643. $this->error = "Exceeded socket read timeout of ".$this->stream_timeout." seconds";
  644. } else {
  645. $this->error = "Exceeded default socket read timeout";
  646. }
  647. $this->progress(HRP_ERROR,$this->error);
  648. return false;
  649. } else {
  650. $this->progress(HRP_ERROR,'No data but not timed out');
  651. }
  652. continue;
  653. }
  654. // check time limits if requested
  655. if ($this->max_time>0) {
  656. if ($this->getmicrotime() - $time_request_start > $this->max_time) {
  657. $this->error = "Exceeded maximum transfer time of ".$this->max_time." seconds";
  658. $this->progress(HRP_ERROR,$this->error);
  659. return false;
  660. break;
  661. }
  662. }
  663. $this->response .= $line;
  664. $iterations++;
  665. if ($headers_received) {
  666. if ($time_connected>0) {
  667. $time_firstdata = $this->getmicrotime();
  668. $process_time = $time_firstdata - $time_connected;
  669. $time_connected = 0;
  670. }
  671. $this->received_bytes += strlen($line);
  672. if ($iterations % 20 == 0) {
  673. $this->update_transfer_counters();
  674. }
  675. }
  676. // some dumbass webservers don't respect Connection: close and just
  677. // leave the connection open, so we have to be diligent about
  678. // calculating the content length so we can disconnect at the end of
  679. // the response
  680. if ( (!$headers_received) && (trim($line)=="") ) {
  681. $headers_received = true;
  682. $this->progress(HRP_DEBUG,"Got headers: {$this->response}");
  683. if (preg_match('/^Content-Length: ([0-9]+)/im',$this->response,$matches)) {
  684. $data_length = (int) $matches[1];
  685. $this->progress(HRP_DEBUG,"Content length is $data_length");
  686. $this->expected_bytes = $data_length;
  687. $this->update_transfer_counters();
  688. } else {
  689. $this->progress(HRP_DEBUG,"No data length specified");
  690. }
  691. if (preg_match("/^Transfer-Encoding: chunked/im",$this->response,$matches)) {
  692. $chunked = true;
  693. $this->progress(HRP_DEBUG,"Chunked transfer encoding requested");
  694. } else {
  695. $this->progress(HRP_DEBUG,"CTE not requested");
  696. }
  697. if (preg_match_all("/^Set-Cookie: ((.*?)=(.*?)(?:;s*(.*))?)$/im",$this->response,$cookielist,PREG_SET_ORDER)) {
  698. foreach ($cookielist as $k=>$cookie) $this->cookie_headers .= $cookie[0]."n";
  699. // get the path for which cookies will be valid if no path is specified
  700. $cookiepath = preg_replace('//{2,}/','',$urldata['path']);
  701. if (substr($cookiepath,-1)!='/') {
  702. $cookiepath = explode('/',$cookiepath);
  703. array_pop($cookiepath);
  704. $cookiepath = implode('/',$cookiepath) . '/';
  705. }
  706. // process each cookie
  707. foreach ($cookielist as $k=>$cookiedata) {
  708. list(,$rawcookie,$name,$value,$attributedata) = $cookiedata;
  709. $attributedata = explode(';',trim($attributedata));
  710. $attributes = array();
  711. $cookie = array(
  712. 'value'=>$value,
  713. 'raw'=>trim($rawcookie),
  714. );
  715. foreach ($attributedata as $k=>$attribute) {
  716. list($attrname,$attrvalue) = explode('=',trim($attribute));
  717. $cookie[$attrname] = $attrvalue;
  718. }
  719. if (!isset($cookie['domain']) || !$cookie['domain']) $cookie['domain'] = $urldata['host'];
  720. if (!isset($cookie['path']) || !$cookie['path']) $cookie['path'] = $cookiepath;
  721. if (isset($cookie['expires']) && $cookie['expires']) $cookie['expires'] = strtotime($cookie['expires']);
  722. if (!$this->validate_response_cookie($cookie,$urldata['host'])) continue;
  723. // do not store expired cookies; if one exists, unset it
  724. if ( isset($cookie['expires']) && ($cookie['expires']<time()) ) {
  725. unset($this->response_cookies[ $name ][ $cookie['path'] ]);
  726. continue;
  727. }
  728. $this->response_cookies[ $name ][ $cookie['path'] ] = $cookie;
  729. }
  730. }
  731. }
  732. if ($this->result_close) {
  733. if (preg_match_all("/HTTP/([0-9.]+) ([0-9]+) (.*?)[rn]/",$this->response,$matches)) {
  734. $resultcodes = $matches[2];
  735. foreach ($resultcodes as $k=>$code) {
  736. if ($code!=100) {
  737. $this->progress(HRP_INFO,'HTTP result code received; closing connection');
  738. $this->result_code = $code;
  739. $this->result_text = $matches[3][$k];
  740. fclose($fp);
  741. return ($this->result_code==200);
  742. }
  743. }
  744. }
  745. }
  746. }
  747. if (feof($fp)) $this->progress(HRP_DEBUG,'EOF on socket');
  748. @fclose ($fp);
  749. $this->update_transfer_counters();
  750. if (is_array($this->response_cookies)) {
  751. // make sure paths are sorted in the order in which they should be applied
  752. // when setting response cookies
  753. foreach ($this->response_cookies as $name=>$paths) {
  754. ksort($this->response_cookies[$name]);
  755. }
  756. }
  757. $this->progress(HRP_INFO,'Request complete');
  758. } else {
  759. $this->error = strtoupper($urldata["scheme"])." connection to ".$hostname." port ".$urldata["port"]." failed";
  760. $this->progress(HRP_ERROR,$this->error);
  761. return false;
  762. }
  763. // perform an HTTP/HTTPS request using CURL
  764. } elseif ( !$this->disable_curl && ( ($urldata["scheme"]=="https") || ($this->force_curl) ) ) {
  765. $this->progress(HRP_INFO,'Passing HTTP request for $url to CURL');
  766. $curl_mode = true;
  767. if (!$this->_curl_request($url)) return false;
  768. // unknown protocol
  769. } else {
  770. $this->error = "Unsupported protocol: ".$urldata["scheme"];
  771. $this->progress(HRP_ERROR,$this->error);
  772. return false;
  773. }
  774. $this->raw_response = $this->response;
  775. $totallength = strlen($this->response);
  776. do {
  777. $headerlength = strpos($this->response,"rnrn");
  778. $response_headers = explode("rn",substr($this->response,0,$headerlength));
  779. $http_status = trim(array_shift($response_headers));
  780. foreach ($response_headers as $line) {
  781. list($k,$v) = explode(":",$line,2);
  782. $this->response_headers[trim($k)] = trim($v);
  783. }
  784. $this->response = substr($this->response,$headerlength+4);
  785. /* // Handled in-transfer now
  786. if (($this->response_headers['Transfer-Encoding']=="chunked") && (!$curl_mode)) {
  787. $this->remove_chunkiness();
  788. }
  789. */
  790. if (!preg_match("/^HTTP/([0-9.]+) ([0-9]+) (.*?)$/",$http_status,$matches)) {
  791. $matches = array("",$this->version,0,"HTTP request error");
  792. }
  793. list (,$response_version,$this->result_code,$this->result_text) = $matches;
  794. // skip HTTP result code 100 (Continue) responses
  795. } while (($this->result_code==100) && ($headerlength));
  796. // record some statistics, roughly compatible with CURL's curl_getinfo()
  797. if (!$curl_mode) {
  798. $total_time = $this->getmicrotime() - $time_request_start;
  799. $transfer_time = $total_time - $connect_time;
  800. $this->stats = array(
  801. "total_time"=>$total_time,
  802. "connect_time"=>$connect_time, // time between connection request and connection established
  803. "process_time"=>$process_time, // time between HTTP request and first data (non-headers) received
  804. "url"=>$url,
  805. "content_type"=>$this->response_headers["Content-Type"],
  806. "http_code"=>$this->result_code,
  807. "header_size"=>$headerlength,
  808. "request_size"=>$totallength,
  809. "filetime"=>strtotime($this->response_headers["Date"]),
  810. "pretransfer_time"=>$connect_time,
  811. "size_download"=>$totallength,
  812. "speed_download"=>$transfer_time > 0 ? round($totallength / $transfer_time) : 0,
  813. "download_content_length"=>$totallength,
  814. "upload_content_length"=>0,
  815. "starttransfer_time"=>$connect_time,
  816. );
  817. }
  818. $ok = ($this->result_code==200);
  819. if ($ok) {
  820. // if a page preprocessor is defined, call it to process the page contents
  821. if (is_callable($this->page_preprocessor)) $this->response = call_user_func($this->page_preprocessor,$this,$this->response);
  822. // if caching is enabled, save the page
  823. if ($this->caching) $this->_cache_store($cachetoken,$url);
  824. }
  825. return $ok;
  826. }
  827. function validate_response_cookie($cookie,$actual_hostname) {
  828. // make sure the cookie can't be set for a TLD, eg: '.com'
  829. $cookiehost = $cookie['domain'];
  830. $p = strrpos($cookiehost,'.');
  831. if ($p===false) return false;
  832. $tld = strtolower(substr($cookiehost,$p+1));
  833. $special_domains = array("com", "edu", "net", "org", "gov", "mil", "int");
  834. $periods_required = in_array($tld,$special_domains) ? 1 : 2;
  835. $periods = substr_count($cookiehost,'.');
  836. if ($periods<$periods_required) return false;
  837. if (substr($actual_hostname,0,1)!='.') $actual_hostname = '.'.$actual_hostname;
  838. if (substr($cookiehost,0,1)!='.') $cookiehost = '.'.$cookiehost;
  839. $domain_match = (
  840. ($actual_hostname==$cookiehost) ||
  841. (substr($actual_hostname,-strlen($cookiehost))==$cookiehost)
  842. );
  843. return $domain_match;
  844. }
  845. function build_headers() {
  846. $headers = "";
  847. foreach ($this->headers as $name=>$value) {
  848. $value = trim($value);
  849. if (empty($value)) continue;
  850. $headers .= "{$name}: {$value}rn";
  851. }
  852. if (isset($this->request_cookies) && is_array($this->request_cookies)) {
  853. $cookielist = array();
  854. foreach ($this->request_cookies as $name=>$value) {
  855. $cookielist[] = "{$name}={$value}";
  856. }
  857. if (count($cookielist)) $headers .= "Cookie: ".implode('; ',$cookielist)."rn";
  858. }
  859. $headers .= "rn";
  860. return $headers;
  861. }
  862. // opposite of parse_url()
  863. function rebuild_url($urlinfo) {
  864. $url = $urlinfo['scheme'].'://';
  865. if ($urlinfo['user'] || $urlinfo['pass']) {
  866. $url .= $urlinfo['user'];
  867. if ($urlinfo['pass']) {
  868. if ($urlinfo['user']) $url .= ':';
  869. $url .= $urlinfo['pass'];
  870. }
  871. $url .= '@';
  872. }
  873. $url .= $urlinfo['host'];
  874. if ($urlinfo['port']) $url .= ':'.$urlinfo['port'];
  875. $url .= $urlinfo['path'];
  876. if ($urlinfo['query']) $url .= '?'.$urlinfo['query'];
  877. if ($urlinfo['fragment']) $url .= '#'.$urlinfo['fragment'];
  878. return $url;
  879. }
  880. function _replace_hostname(&$url,$new_hostname) {
  881. $parts = parse_url($url);
  882. $old_hostname = $parts['host'];
  883. $parts['host'] = $new_hostname;
  884. $url = $this->rebuild_url($parts);
  885. return $old_hostname;
  886. }
  887. function _curl_request($url) {
  888. $this->error = false;
  889. // if a direct connection IP address was specified, replace the hostname
  890. // in the URL with the IP address, and set the Host: header to the
  891. // original hostname
  892. if ($this->connect_ip) {
  893. $old_hostname = $this->_replace_hostname($url,$this->connect_ip);
  894. $this->headers["Host"] = $old_hostname;
  895. }
  896. unset($this->headers["Content-Length"]);
  897. $headers = explode("n",$this->build_headers());
  898. $ch = curl_init();
  899. curl_setopt($ch,CURLOPT_URL, $url); 
  900. curl_setopt($ch,CURLOPT_USERAGENT, $this->headers["User-Agent"]); 
  901. curl_setopt($ch,CURLOPT_HEADER, 1); 
  902. curl_setopt($ch,CURLOPT_RETURNTRANSFER, 1); 
  903. // curl_setopt($ch,CURLOPT_FOLLOWLOCATION, 1); // native method doesn't support this yet, so it's disabled for consistency
  904. curl_setopt($ch,CURLOPT_TIMEOUT, 10);
  905. if ($this->curl_proxy) {
  906. curl_setopt($ch,CURLOPT_PROXY,$this->curl_proxy);
  907. }
  908. curl_setopt($ch,CURLOPT_HTTPHEADER, $headers);
  909. if ($this->method=="POST") {
  910. curl_setopt($ch,CURLOPT_POST,1);
  911. curl_setopt($ch,CURLOPT_POSTFIELDS,$this->post_data);
  912. }
  913. if ($this->insecure_ssl) {
  914. curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,0);
  915. }
  916. if ($this->ignore_ssl_hostname) {
  917. curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,1);
  918. }
  919. $this->response = curl_exec ($ch);
  920. if (curl_errno($ch)!=0) {
  921. $this->error = "CURL error #".curl_errno($ch).": ".curl_error($ch);
  922. }
  923. $this->stats = curl_getinfo($ch);
  924. curl_close($ch);
  925. return ($this->error === false);
  926. }
  927. function progress($level,$msg) {
  928. if (is_callable($this->progress_callback)) call_user_func($this->progress_callback,$level,$msg);
  929. }
  930. // Gets any available HTTPRetriever error message (including both internal
  931. // errors and HTTP errors)
  932. function get_error() {
  933. return $this->error ? $this->error : 'HTTP ' . $this->result_code.': '.$this->result_text;
  934. }
  935. function get_content_type() {
  936. if (!$ctype = $this->response_headers['Content-Type']) {
  937. $ctype = $this->response_headers['Content-type'];
  938. }
  939. list($ctype,) = explode(';',$ctype);
  940. return strtolower($ctype);
  941. }
  942. function update_transfer_counters() {
  943. if (is_callable($this->transfer_callback)) call_user_func($this->transfer_callback,$this->received_bytes,$this->expected_bytes);
  944. }
  945. function set_transfer_display($enabled = true) {
  946. if ($enabled) {
  947. $this->transfer_callback = array(&$this,'default_transfer_callback');
  948. } else {
  949. unset($this->transfer_callback);
  950. }
  951. }
  952. function set_progress_display($enabled = true) {
  953. if ($enabled) {
  954. $this->progress_callback = array(&$this,'default_progress_callback');
  955. } else {
  956. unset($this->progress_callback);
  957. }
  958. }
  959. function default_progress_callback($severity,$message) {
  960. $severities = array(
  961. HRP_DEBUG=>'debug',
  962. HRP_INFO=>'info',
  963. HRP_ERROR=>'error',
  964. );
  965. echo date('Y-m-d H:i:sa').' ['.$severities[$severity].'] '.$message."n";
  966. flush();
  967. }
  968. function default_transfer_callback($transferred,$expected) {
  969. $msg = "Transferred " . round($transferred/1024,1);
  970. if ($expected>=0) $msg .= "/" . round($expected/1024,1);
  971. $msg .= "KB";
  972. if ($expected>0) $msg .= " (".round($transferred*100/$expected,1)."%)";
  973. echo date('Y-m-d H:i:sa')." $msgn";
  974. flush();
  975. }
  976. function getmicrotime() { 
  977. list($usec, $sec) = explode(" ",microtime()); 
  978. return ((float)$usec + (float)$sec); 
  979. }
  980. }
  981. ?>