Fix PHP Requests library
起因
前几天因为某些需要,使用了PHP的一个requests
库,Github:https://github.com/rmccue/Requests,说是Requests for PHP is a humble HTTP request library
。
用了一段时间感觉挺不错的,缺点就是文档,很不清晰,例如get方法的options参数,只点明了是个数组,但是具体有哪些可配置项并没有明确的说明。
无意间发现了一个bug,简单的讲,就是在get一个302的网页时,如果出现Location: ../test.php
这种形式,requests库会抛出一个异常:
Fatal error: Uncaught exception 'Requests_Exception' with message 'Only HTTP requests are handled.' in D:\wamp64\www\test\vendor\rmccue\requests\library\Requests.php on line 480
这就十分尴尬了,所以决定自己修一下。
修bug
先搞个测试环境,写几个php文件复现一下:
redirect.php
<?php
header("Location: ../test.php");
echo 111111;
test.php随便写点什么,输出个hello world之类的。
fixRequests.php
<?php
require_once "vendor/autoload.php";
Requests::register_autoloader();
$r = Requests::get("http://127.0.0.1/test/redirect.php");
# $r = Requests::get("http://127.0.0.1/test.php");
var_dump($r->body);
访问http://127.0.0.1/test/fixRequests.php,果然抛出了异常。我们追踪一下get函数。
在/vendor/rmccue/requests/library/Request.php的183行。
/**#@+
* @see request()
* @param string $url
* @param array $headers
* @param array $options
* @return Requests_Response
*/
/**
* Send a GET request
*/
public static function get($url, $headers = array(), $options = array()) {
return self::request($url, $headers, null, self::GET, $options);
}
继续追踪request
函数,在297行。
public static function request($url, $headers = array(), $data = array(), $type = self::GET, $options = array()) {
if (empty($options['type'])) {
$options['type'] = $type;
}
$options = array_merge(self::get_default_options(), $options);
self::set_defaults($url, $headers, $data, $type, $options);
$options['hooks']->dispatch('requests.before_request', array(&$url, &$headers, &$data, &$type, &$options));
if (!empty($options['transport'])) {
$transport = $options['transport'];
if (is_string($options['transport'])) {
$transport = new $transport();
}
}
else {
$transport = self::get_transport();
}
$response = $transport->request($url, $headers, $data, $options);
$options['hooks']->dispatch('requests.before_parse', array(&$response, $url, $headers, $data, $type, $options));
return self::parse_response($response, $url, $headers, $data, $options);
}
可以看到在317行调用$transport->request获取了结果,之后调用parse_response解析结果。
猜测是在这里解析的response header,发现有302状态码,然后去进行跳转相关的操作。所以继续跟进。
protected static function parse_response($headers, $url, $req_headers, $req_data, $options) {
// 省略无关代码。。。
if ((in_array($return->status_code, array(300, 301, 302, 303, 307)) || $return->status_code > 307 && $return->status_code < 400) && $options['follow_redirects'] === true) {
if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) {
if ($return->status_code === 303) {
$options['type'] = Requests::GET;
}
$options['redirected']++;
$location = $return->headers['location'];
if (strpos ($location, '/') === 0 ) {
// relative redirect, for compatibility make it absolute
$location = Requests_IRI::absolutize($url, $location);
$location = $location->uri;
}
$redirected = self::request($location, $req_headers, $req_data, false, $options);
$redirected->history[] = $return;
return $redirected;
}
elseif ($options['redirected'] >= $options['redirects']) {
throw new Requests_Exception('Too many redirects', 'toomanyredirects', $return);
}
}
$return->redirects = $options['redirected'];
$options['hooks']->dispatch('requests.after_request', array(&$return, $req_headers, $req_data, $options));
return $return;
}
可以看到就在这部分进行的跳转处理,发现关键的部分:
if (strpos ($location, '/') === 0 ) {
// relative redirect, for compatibility make it absolute
$location = Requests_IRI::absolutize($url, $location);
$location = $location->uri;
}
这里把Location格式化成了绝对URL的形式,但是作者只考虑到了以/
开头的形式,没考虑到../
这种。
所以我们自己修改一下就好了。
if (strpos ($location, '/') === 0 || strpos($location, '../') ) {
// relative redirect, for compatibility make it absolute
$location = Requests_IRI::absolutize($url, $location);
$location = $location->uri;
}
再测试一下302跳转,发现已经可以正常使用了。
顺便发现了两个参数
看代码的过程中发现了options的两个参数,一个是follow_redirects,另一个是redirects
根据代码含义可以看出,follow_redirects是决定是否跟随302跳转的,而redirects是决定最大跳转次数的。
作者考虑的挺周到的,但是为什么文档那么差啊!(╯‵□′)╯︵┻━┻