实例:磁力链接转种子
网址:https://itorrents.org/
需求:通过该网站,根据磁力链接下种子文件
一般的下载格式:
请求地址:http://itorrents.org/torrent/INFO_HASH_IN_HEX.torrent
请求实例:http://itorrents.org/torrent/B415C913643E5FF49FE37D304BBB5E6E11AD5101.torrent
更新
【20200406】itorrents网站的Cloudflare升级之后,脚本出现
+Function("return escape")()(("")["italics"]())[2]+"o"+(undefined+"")[2]+(t
等等,可读性降低,在简化JS脚本时需要更多措施
必要Cookie
- __cfduid:这个cookie在返回503时候从请求头得到,用于之后的认证过程
- cf_clearance: 访问网站真正使用的cookie,请求头加入它,可以认证通过并正常访问到网站,该cookie的存活时间为2小时
代码获取cookie
使用的平台和工具:
- 平台:JRE 8 或 Android 6.0
- 软件:Postman,模拟http请求
第三方jar包:
- jsoup: 爬取网页源代码,包括html和Js
- rhino: 让安卓能够执行js脚本
- okhttp: 用于发网络请求,获取cookie,下种子文件等操作
具体过程:
首先代码发送请求,让服务器返回503。
//创建OkHttpClient对象
OkHttpClient client = new OkHttpClient.Builder().build();
//创建Request对象,设置一个url地址, 设置请求方式。
Request request = new Request.Builder()
.addHeader("User-Agent", USER_AGENT)
.url("https://itorrents.org").method("GET", null)
.build();
//创建一个call对象,参数就是Request请求对象
Call call = client.newCall(request);
//同步调用,返回Response,会抛出IO异常
Response response = call.execute();
出现503错误,首先从返回头部中获取第一个cookie( __cfduid)的值
Headers headers = response.headers();
String setCookieStr = headers.get("Set-Cookie");
String __cfduid = setCookieStr.split(";")[0].trim();
使用Jsoup爬取返回的网页数据
// 调用该句获取返回输入流
InputStream is = response.body().byteStream();
private String getErrorHtml(InputStream is){
String resultHtml = "";
try{
BufferedReader reader = new BufferedReader(new InputStreamReader(is, "utf-8"));
String line = null;
while ((line = reader.readLine()) != null) {
resultHtml += line;
}
} catch (IOException e){
EvLog.e(TAG, "IOException: " + e.getMessage());
}
return resultHtml;
}
//获取表单所有参数
private Map<String, String> getNewHttpParams(String resultHtml) throws Exception {
Document doc = Jsoup.parse(resultHtml);
//取得表单
Element loginForm = doc.getElementById("challenge-form");
//取得script下面的JS变量
Element js = doc.getElementsByTag("script").get(0);
//过滤JS内容,只保留有效内容
String jsData = js.data();
jsData = jsData.substring(jsData.indexOf("setTimeout"), jsData.indexOf("'; 121'"));
jsData = "var " + jsData.substring(jsData.indexOf("f,") + 2).trim();
String tiStr = jsData.substring(jsData.indexOf("t = document.createElement('div');"), jsData.indexOf("challenge-form") + 18);
jsData = jsData.replace(tiStr, "").replace("a.value", "var a").replace("t.length", "13");
//...执行JS代码
return params;
}
观察返回结果:只有第四个参数的值没有体现
<form id="challenge-form" action="/cdn-cgi/l/chk_jschl" method="get">
<input type="hidden" name="s" value="79e2e9582b532dc8c1803b475404232b947dd65e-1552293281-1800-AaPJ3gbam22HO+RRAxJmBJLQP/wyX5Gxit+iGz77w0hI8Ph74sfEKxz5xGm3RWVJzpTVdlM93S4QTzpDHf6HgUeyLd3E1kiC0B0d7rUOEKmd"></input>
<input type="hidden" name="jschl_vc" value="17505afb059e796938098575225488f4"/>
<input type="hidden" name="pass" value="1552293285.342-v/1sNwvefM"/>
<input type="hidden" id="jschl-answer" name="jschl_answer"/>
</form>
第四个参数需要分析返回网页的JS,代码如下
//
<![CDATA[
(function(){
var a = function() {try{return !!window.addEventListener} catch(e) {return !1} },
b = function(b, c) {a() ? document.addEventListener("DOMContentLoaded", b, c) : document.attachEvent("onreadystatechange", b)};
b(function(){
var a = document.getElementById('cf-content');a.style.display = 'block';
setTimeout(function(){
var s,t,o,p,b,r,e,a,k,i,n,g,f, XcSJxuC={"sKGOcaDtX":+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![])+(+[])+(!+[]+!![]+!![]+!![])+(!+[]+!![])+(!+[]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]))/+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(+!![])+(!+[]+!![]+!![]+!![]+!![])+(+[])+(+[]))};
t = document.createElement('div');
t.innerHTML="<a href='/'>x</a>";
t = t.firstChild.href;r = t.match(/https?:\/\//)[0];
t = t.substr(r.length); t = t.substr(0,t.length-1);
a = document.getElementById('jschl-answer');
f = document.getElementById('challenge-form');
;XcSJxuC.sKGOcaDtX-=+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(!+[]+!![]+!![]+!![])+(+!![])+(+[])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(+!![]))/+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(!+[]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![])+(+[])+(+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]));a.value = +XcSJxuC.sKGOcaDtX.toFixed(10) + t.length; '; 121'
f.action += location.hash;
f.submit();
}, 4000);
}, false);
})();
//]]>
分析可以发现,变量a存储第四个参数值,f是提交的表单。因此,如何得到a变量的值是关键
a.value = +XcSJxuC.sKGOcaDtX.toFixed(10) + t.length;
由两个部分组成,一个是XcSJxuC.sKGOcaDtX,另一个是t.length,首先解决t.length,t的值经过下面几个操作
t = document.createElement('div'); //创建div标签
t.innerHTML="<a href='/'>x</a>"; //添加子元素
t = t.firstChild.href; //获得a标签的href属性,这里是网站根路径https://itorrents.org/
r = t.match(/https?:\/\//)[0]; //获取t的https://前缀
t = t.substr(r.length); //获取子串 itorrents.org/
t = t.substr(0,t.length-1); //去掉子串最后一个字符'/',结果 itorrents.org
因此t.length值为13。每次返回的这一结果固定,因此直接替换成13即可
解决XcSJxuC.sKGOcaDtX的问题,将js简化,跟其有关的只有如下操作
XcSJxuC={"sKGOcaDtX":+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![])+(+[])+(!+[]+!![]+!![]+!![])+(!+[]+!![])+(!+[]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]))/+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(+!![])+(!+[]+!![]+!![]+!![]+!![])+(+[])+(+[]))};
XcSJxuC.sKGOcaDtX-=+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(!+[]+!![]+!![]+!![])+(+!![])+(+[])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(+!![]))/+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(!+[]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![])+(+[])+(+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]));
/*
[],数组符号,默认值为0,但不能单独使用
!, 取反符号,本身不具备数值意义,不能单独使用,需要和[]配合使用
!+[],取反加数组,值为1
(+[]), 加数组,值为0
+![],取反数组,值为0
+!![],二次取反数组,值为1
var b = +((!+[]+!![]+![])+(![]+![]+!![]+!+[])); 值为4
*/
因此,在理论上是可以通过执行JS脚本把值算出来的,使用Jsoup获取全部脚本后,采取字符串操作将js过滤成如下代码:
var XcSJxuC={"sKGOcaDtX":+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![])+(+[])+(!+[]+!![]+!![]+!![])+(!+[]+!![])+(!+[]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]))/+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(+!![])+(!+[]+!![]+!![]+!![]+!![])+(+[])+(+[]))};
XcSJxuC.sKGOcaDtX-=+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(!+[]+!![]+!![]+!![])+(+!![])+(+[])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(+!![]))/+((!+[]+!![]+!![]+!![]+!![]+!![]+!![]+[])+(!+[]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![])+(+[])+(+!![])+(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![]+!![])+(!+[]+!![]+!![]));
var a = +XcSJxuC.sKGOcaDtX.toFixed(10) + 13;
最后返回的 a 就是第四个参数的值,因此,使用rhino库 ,写一个方法,让android能执行js代码,并获取结果
// 执行JS脚本,获得所需参数的值
private String getJsAnswer(String jsString) {
//Enter a Context
org.mozilla.javascript.Context context = org.mozilla.javascript.Context.enter();
context.setOptimizationLevel(-1);
try{
//Initializing standard objects
Scriptable scope = context.initStandardObjects();
ScriptableObject.putProperty(scope, "javaContext",
org.mozilla.javascript.Context.javaToJS(org.mozilla.javascript.Context.getCurrentContext(), scope));
ScriptableObject.putProperty(scope, "javaLoader",
org.mozilla.javascript.Context.javaToJS(getClass().getClassLoader(), scope));
//Evaluating a script
context.evaluateString(scope, jsString, HOST, 1, null);
Object a = scope.get("a", scope);
if (a == Scriptable.NOT_FOUND) {
EvLog.e(TAG, "answer not found.");
} else {
return org.mozilla.javascript.Context.toString(a);
}
} catch (Exception e){
EvLog.e(TAG, "Exception: " + e.getMessage());
} finally {
org.mozilla.javascript.Context.exit();
}
return null;
}
如果是纯javaSE或者javaEE项目,支持javax,其自带js引擎可以帮助你执行js代码,更加方便,如下:
// 执行JS脚本,获得所需参数的值
private String getJsAnswer(String jsString) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
try {
engine.eval(jsString);
return engine.get("a") + "";
} catch (ScriptException e) {
e.printStackTrace();
}
return null;
}
最后算出来结果,比如最后算出来jschl_answer为22.6738726886,结合前三个参数,使用okhttp包,GET方式提交表单到指定地址,这里需要注意延迟4秒再发送请求。
请求头要带上第一个获取的cookie __cfduid,代码如下
//模拟cloudflare延时4秒,以便产生新的cookie,也标志着此为耗时操作,只能在子线程中完成
Thread.sleep(4 * 1000);
OkHttpClient client = new OkHttpClient.Builder().cookieJar(new CookieJar() {
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
List<Cookie> list = cookieStore.get(HOST);
if(list.addAll(cookies)){
EvLog.i(TAG, "saveFromResponse -- 添加进cookiestore");
cookieStore.put(HOST, list);
} else {
EvLog.i(TAG, "saveFromResponse -- 重置cookiestore");
cookieStore.put(HOST, cookies);
}
}
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
List<Cookie> cookies = cookieStore.get(HOST);
return cookies != null ? cookies : new ArrayList<>();
}
}).build();
//创建Request对象,设置一个url地址, 设置请求方式。
Request request = new Request.Builder()
.addHeader("User-Agent", USER_AGENT)
.addHeader("Cookie", __cfduid)
.addHeader("Referer", "https://itorrent.org")
.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8")
.addHeader("Connection", "keep-alive")
.addHeader("Host", "itorrent.org")
.url(jsChl).method("GET", null)
.build();
//创建一个call对象,参数就是Request请求对象
Call call = client.newCall(request);
//同步调用,返回Response,会抛出IO异常
Response response = call.execute();
if(response.code() == HttpURLConnection.HTTP_OK) {
// 获得新的 Cookie
List<Cookie> cookies = cookieStore.get(HOST);
for(Cookie c : cookies) {
if(c.name().equals("cf_clearance")) {
return c;
}
}
} else {
EvLog.e(TAG, "请求失败,错误码: " + response.code());
}
如果返回200,则能拿到真正需要的cf_clearance的值,使用偏好设置将cookie持久化
拿到第二个cookie之后,加入到请求头当中,就可以成功访问并下载种子文件
String downloadUrl = "http://itorrents.org/torrent/B415C913643E5FF49FE37D304BBB5E6E11AD5101.torrent";
//创建OkHttpClient对象
OkHttpClient client = new OkHttpClient.Builder().build();
//创建Request对象,设置一个url地址, 设置请求方式。
Request request = new Request.Builder()
.addHeader("User-Agent", USER_AGENT)
.addHeader("Cookie", cookie)
.url(downloadUrl).method("GET", null)
.build();
//创建一个call对象,参数就是Request请求对象
Call call = client.newCall(request);
//同步调用,返回Response,会抛出IO异常
Response response = call.execute();
//获取返回数据
if(response.code() == HttpURLConnection.HTTP_OK) {
FileUtils.copyInputStreamToFile(response.body().byteStream(), saveTo);
EvLog.i(TAG, "Download torrent success!!!");
}