请注意:本页内容发布于 3258 天前,内容可能已经过时,请注意甄别。
这个东西的最初设计目标是:在不对某些人取消关注(加黑名单也会取消关注)的情况下,通过中间人攻击(不是唬人,其实本质就是篡改返回结果)为不支持消息过滤的饭否客户端(实际上,没有任何一款饭否客户端和官方界面有这个功能,据本人所知的唯一途径是Chrome插件Fanatic,但也只能对饭否网页版起作用)提供相应功能。
由于没有一款客户端支持自定义API地址,所以采取的方式是:在本地用hosts将API所在主机(api.fanfou.com)重定向,然后在目标服务器接受解析,等于欺骗一下客户端,让它以为连接的是官方的API服务器。
此外,既然是中间人攻击,那么其实想对返回结果做什么都可以,本来除了过滤特定人之外还想加入更多功能,可是写完就懒了。
代码写得很面条很烂,请多包涵。
使用方法(以Apache 2.4.4 + PHP 5.3为例):
1、将文件内容另存为UTF-8无BOM格式,文件名为index.php(或任何目标服务器支持的默认文档名)
2、确认PHP开启cURL扩展,Apache开启了Rewrite扩展,修改httpd-vhosts.conf(或Apache虚拟主机配置文件所在处):
<VirtualHost *:80> ServerName api.fanfou.com DocumentRoot "文件所在路径" </VirtualHost> <Directory "文件所在路径"> Order Allow,Deny Allow from All RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f #-f的含意是将请求的路径当作文件来看待,由于-f会检测文件是否真实存在,所以!之 RewriteRule ^(.*)$ index.php?reqf=$1 [QSA] #QSA == Query strings appended,即将原始Query String内容原样转发 </Directory>
3、配置主机,将api.fanfou.com绑定到刚刚配置的Apache服务器上。
4、在要使用该代理的客户端上,配置hosts文件,将api.fanfou.com指向文件所在的主机。
2016-02-03更新:
在调试自己的饭否Lib时,发现任何返回的HTTP状态码都是200,检查一下发现是代理没有转发饭否API服务器返回的状态码,已在关闭cURL句柄前进行处理。
2016-01-13更新:
1.当发送图片信息(此时Content-Type为multipart/form-data),且消息内容开头为@时,status参数会被cURL认为是文件指向,造成出现can’t open file错误,导致消息发送失败。本来以为因为改动POST内容,会导致OAuth签名失效,解决这个问题会很麻烦,以致做好了要反编译客户端、拿OAuth Consumer信息的准备,结果第一次试探即告成功,解决方法竟然是在status数据的开头加一个空格就可以了,而且签名似乎仍然有效,并不会影响消息的成功发送,真是神奇。
2.解决之前版本错将Debug开关写成局部变量$debug,导致无论如何都不会输出日志的问题,现已修改为常量DEBUG_MODE。
<?php
const DEBUG_MODE = TRUE; //调试开关,设置为 true 以输出日志
//获取 Rewrite 传入的 Query string 参数 reqf ,包含了客户端原始请求的API路径
if (!$_GET['reqf'] || empty($_GET['reqf'])) die("No API path to request specified!");
//获取调用的API模式,用于处理返回时快速跳转相应处理模块
$api_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$api_name = strtolower(pathinfo($api_path) ['filename']);
$api_ext = strtolower(pathinfo($api_path) ['extension']);
writelog("Requested API extension: " . $api_ext);
//获取客户端请求 Method
$method = strtoupper($_SERVER['REQUEST_METHOD']);
//构造远端 API 请求 URL
$api_url = "http://api.fanfou.com/" . $_GET['reqf'];
//整理要转发的 headers
$request_headers = array();
foreach (apache_request_headers() as $arhk => $arhv) {
writelog($method . " Header [" . $arhk . "] => " . $arhv);
$request_headers[] = $arhk . ': ' . $arhv;
}
//如果有 Query string 参数,构造一个去掉 reqf 项的 GET 参数数组
$clientQS = '';
if ($_GET && sizeof($_GET) > 1) {
foreach ($_GET as $gk => $gv) {
if (strtolower($gk) != 'reqf') $clientQS = $clientQS . '&' . $gk . '=' . $gv;
}
if (substr($clientQS, 0, 1) == '&') $clientQS = '?' . substr($clientQS, 1);
$api_url = $api_url . $clientQS;
}
//对 URL 中的空格替换为+
//这一行的主要用途是修复蘑菇饭提交带图消息时,在Query String中又写了一遍已经在POST里写过的status参数,却又不将空格转化成+(西文加号),导致签名不符过不了OAuth验证
$api_url = str_replace(" ", "+", $api_url);
writelog($method . ' ' . $api_url);
//准备 cURL 对象
$c = curl_init();
curl_setopt($c, CURLOPT_URL, $api_url);
curl_setopt($c, CURLOPT_ENCODING, 'gzip,deflate');
curl_setopt($c, CURLOPT_TIMEOUT_MS, 3000);
curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
//根据 Method 不同进行不同的 curl 操作
switch ($method) {
case 'GET':
//GET:追加header
curl_setopt($c, CURLOPT_HTTPHEADER, $request_headers);
break;
case 'POST':
//POST 将要提交的数据整理后追加,分情况处理
curl_setopt($c, CURLOPT_POST, true);
if (strpos(strtolower($_SERVER["CONTENT_TYPE"]) , "multipart/form-data") !== FALSE) {
//按multipart/form-data处理
writelog("Posting multipart/form-data content...");
//针对cURL重写的请求对header进行修正
for ($rhidx = 0; $rhidx < sizeof($request_headers); $rhidx++) {
//删除原有的Content-Type参数(boundary会被重复添加,与原始值不一致,导致服务器无法分割数据)
if (stristr($request_headers[$rhidx], "Content-Type") !== FALSE) $request_headers[$rhidx] = '';
//删除原有的Content-Length参数(cURL对请求重写后与原始长度不一致,如果原始值比新值长,服务器就会一致等待未传输完成的数据,直到超时)
if (stristr($request_headers[$rhidx], "Content-Length") !== FALSE) $request_headers[$rhidx] = '';
}
//Debug模式下输出整理后的Request Header
if (DEBUG_MODE === TRUE) {
foreach ($request_headers as $rh) {
writelog("Request header: " . $rh);
}
}
curl_setopt($c, CURLOPT_HTTPHEADER, $request_headers);
//重新组织POST数据,主要为了将上传的文件重新附加
$postdata = array();
foreach ($_POST as $pdk => $pdv) {
writelog("POST data: [" . $pdk . "] => " . $pdv);
$postdata[$pdk] = $pdv;
}
//如果提交的数据有文件数据(对饭否来说只有一种情况:photo),PHP会将其按 $_FILES(POST提交的文件)处理,需要重新编入准备提交的数据
if ($_FILES['photo']) {
writelog("File uploaded as " . $_FILES['photo']['tmp_name']);
$postdata['photo'] = '@' . $_FILES['photo']['tmp_name'] . ';filename=' . $_FILES['photo']['name'] . ';type=' . $_FILES['photo']['type'];
if(strpos($postdata['status'], '@') === 0) $postdata['status'] = " ".$postdata['status'];
}
writelog("POST data in a nutshell: ".var_export($postdata, TRUE));
curl_setopt($c, CURLOPT_POSTFIELDS, $postdata);
} else {
//按application/x-www-form-urlencoded处理
writelog("POST data: " . http_build_query($_POST));
curl_setopt($c, CURLOPT_HTTPHEADER, $request_headers);
curl_setopt($c, CURLOPT_POSTFIELDS, http_build_query($_POST));
}
break;
default:
//除此之外的方法都不支持
die("Unsupported request method [" . $method . "]!");
}
//提交请求并获取返回
$output = curl_exec($c);
writelog($method . " executed.");
if ($output === false) {
writelog($method . " cURL error: " . curl_error($c));
}
//设置返回的HTTP Status Code
http_response_code(curl_getinfo($c, CURLINFO_HTTP_CODE));
//关闭 curl 句柄
curl_close($c);
//处理结果
switch ($api_ext) {
case 'xml':
//使用 simpleXML 加载返回内容
$xml = simplexml_load_string($output);
//如果试图打开 XML 出错,原样返回,并在日志中记录
if ($xml === FALSE) {
writelog("Error parsing XML return...");
break;
}
//由于暂时用不到 XML 过滤,什么也没有写,有需求可自行扩展
break;
case 'json':
//只对特定的 API 进行处理
if ($api_name == "home_timeline" || $api_name == "mentions" || $api_name == "public_timeline") {
//将 JSON 返回解析为对象数组
$json_array = json_decode($output);
//如果试图打开 JSON 出错,原样返回,并在日志中记录
if (json_last_error() !== JSON_ERROR_NONE) {
writelog("Error parsing JSON return...");
break;
}
//开始处理JSON
//范例:过滤各种 Timeline 中,user 的 id 为 ifanfou 的消息
foreach ($json_array as $json_ek => $json_ev) {
if ($json_ev->{'user'}->{'id'} === "ifanfou") unset($json_array[$json_ek]);
}
//以下代码解释:https://stackoverflow.com/questions/20372982/removing-array-index-reference-when-using-json-encode-in-php
//解释:如果上面的foreach实际执行了过滤操作(顺便说一句 foreach 能做到在其循环中删除元素其实是不对的),
//那么 $json_array 的数组索引就会变成不连续的,而不连续的索引会被视为 json 不支持的关联数组,
//所以 json_encode() 会将索引视为一个外层嵌套元素,变成 [ "1": {"created_at": ...} ] 的形式,而客户端是不能解析这种结构的。
//array_values() 的作用是提取数组中的所有值,并为其重新建立数字索引,当数字索引的值连续起来后,再进行 json_encode() 就可以得到需要的输出了。
$json_array = array_values($json_array);
//重新将数组编译为JSON字符串
$output = json_encode($json_array);
}
break;
default: //如果是饭否API只剩下一种可能:rss,不进行处理
break;
}
//输出结果
echo ($output);
//本地方法:写入日志
function writelog($s) {
if (DEBUG_MODE === TRUE) {
$s = (string)$s;
$s = '[' . date('Y-m-d H:i:s') . '] ' . $s;
$f = fopen("log_" . date('Ymd') . ".txt", "a");
fwrite($f, "\r\n" . $s);
fclose($f);
}
}
?>
话说难道你的工作是饭否的开发么?
你可以理解为我纯属闲得没事干。
多喝热水,就不咸了。
热水烫口,凉开更佳。(这种对话也算闲的一种吧