记录一次Surge & QuantumultX 脚本开发过程

文章推薦指數: 80 %
投票人數:10人

前言本文主要记录一次Surge或QuantumultX的脚本开发过程, 过程包括抓包、分析、调试、以及编写脚本. 记录内容为哔哩哔哩漫画积分商城自动抢券脚本. 主页归档分类标签2021-07-16发表2021-07-17更新脚本开发28分钟读完(大约4218个字)0次浏览记录一次Surge&QuantumultX脚本开发过程前言 本文主要记录一次Surge或QuantumultX的脚本开发过程,过程包括抓包、分析、调试、以及编写脚本. 记录内容为哔哩哔哩漫画积分商城自动抢券脚本. 背景作为一个合格的二次元迷,漫画当然是少不了🧐 国内日漫大多都被哔哩哔哩漫画拿下版权,然而里面大多数的日漫都需要氪金或使用漫读券才能看; 最近发现使用签到脚本获得的积分囤得差不多了,换漫读券又可以省一笔,无奈老是错过相关商品兑换时间,一气之写了个脚本让它在规定时间内自动抢券. 过程第一步:抓包本文将使用Thor进行抓包,并使用Anubis进行重放演示,简单分析哔哩哔哩漫画的网络请求. 我们进入哔哩哔哩漫画APP后,打开Thor开启抓包,返回APP积分商城随便兑换一个东西,再返回Thor关闭抓包. Thor有着完备的关键字过滤,刚刚兑换了75积分的商品,我们可以尝试搜索请求体和响应体内的关键字,看看是否有结果. 搜索后一个名为Pointshop/Exchange的请求直接映入眼下,翻译成中文大意为店铺/兑换,很直观. 我们进一步查看该请求的请求体,以及响应体. 可以很直观的看到请求体中的各种参数,product_id表示兑换的商品,product_num表示兑换的数量,point表示消耗的积分. 响应体中code为0表示兑换成功,expire_day表示有效期,remain_amount表示该商品库存. 我们把该请求使用anubis重放,看看该接口是否有效. 可以看到重放后该接口是可用的,商品剩余数量相应减少,返回app查看账号积分也减少了75并收到商品. 之后我们看一下请求体中的product_id的商品id是怎么来的,返回Thor之前抓到的包,筛选器搜索1048关键字. 搜索后有一个叫ListProduct的请求,翻译成中文大意为商品清单,我们点开响应体可以看到一个商品名为”小智怪谈”的商品,商品id为1048,商品库存为3796,正是我之前所兑换的商品. 该接口使用anubis重放后并没有什么问题; 最后还差一个查询账户积分的接口,我们使用Thor筛选器搜索响应关键字,开启抓包之前我的账户有3172积分,则尝试搜索3172 可以看到有一个叫GetUserPoint的请求,翻译成中文大意为获取用户积分,点开响应后我们可以看到查询到的账户积分. 第二步:分析我们抓到接口后使用anubis重放进一步分析: 精简参数,分析url中的device之类的参数是否必须、是否验证Cookie、请求体是否必须,以减少脚本编写工作量. 分析各种情况下接口返回不同响应的可能性,供脚本正确判断. 分析查询积分接口12原接口https://manga.bilibili.com/twirp/pointshop.v1.Pointshop/GetUserPoint?device=h5&platform=web 经过各种重放后可得知 接口可省略url中的参数. 请求体可省略. 必须使用POST方法. 使用请求头中的Cookie字段作为用户鉴权,一些非必要字段也可省略. 带有效Cookie响应体内容为 1234567{"code":0,"msg":"","data":{"point":"用户实际积分数量"}} Cookie失效后响应体内容为 1234567{"code":0,"msg":"","data":{"point":"0"}} 分析查询商品接口12原接口https://manga.bilibili.com/twirp/pointshop.v1.Pointshop/ListProduct?device=h5&platform=web 经过各种重放后可得知 接口可省略url中的参数. 请求体可省略. 必须使用POST方法. 无需用户鉴权. 响应体: 123456789101112131415161718192021222324252627{"code":0,"msg":"","data":[{"id":195,"type":7,"title":"积分兑换","image":"","amount":15999,"cost":200,"real_cost":100,"remain_amount":0,"comic_id":0,"limits":[],"discount":0,"product_type":1,"pendant_url":"","pendant_expire":0,"exchange_limit":0,"address_deadline":"0001-01-01T00:00:00Z","act_type":0,"has_exchanged":false,"main_coupon_deadline":"0001-01-01T00:00:00Z","deadline":"","point":"0"}]} 以上非实际响应,其他商品过多,已省略. 分析兑换商品接口12原接口https://manga.bilibili.com/twirp/pointshop.v1.Pointshop/Exchange?device=h5&platform=web 经过各种重放后可得知 接口可省略url中的参数. 请求体需带有 12345{"product_id":"商品ID","product_num":"商品兑换数量","point":"总消耗积分数量"} 必须使用POST方法. 使用请求头中的Cookie字段作为用户鉴权,一些非必要字段也可省略. 兑换成功响应体内容为 12345678910{"code":0,"msg":"","data":{"id":"商品使用ID","expire_day":"商品过期剩余天","remain_amount":"商品库存","deadline":"0001-01-01T00:00:00Z"}} 第三步:编写脚本 什么是函数 函数是JavaScript中的基本组件之一,一个函数是JavaScript过程中执行一组任务或计算值的语句. 本文的抢券脚本将定义各种函数以方便统一调用. 查看JavaScript函数详细参考文档了解更多. 应用兼容由于该脚本针对多平台,脚本的写法需要同时兼容Surge或QuanX之类的客户端,那么我们就需要写一个兼容函数让它在不同环境下也能被正确执行. 以下函数兼容Surge、QX、Loon中的部分API,包括持久化读取、通知、POST请求 兼容函数(点击左边箭头展开)>folded123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869//const$=newnobyda();//发送一个通知:$.notify('title','subtitle','message')//持久化读取:$.read('Key')//POST请求:$.post(url,callback)functionnobyda(){ constisSurge=typeof$httpClient!="undefined"; constisQuanX=typeof$task!="undefined"; constisNode=typeofrequire=="function"; constnode=(()=>{ if(isNode){ constrequest=require('request'); return{ request } }else{ returnnull; } })() constadapterStatus=(response)=>{ if(response){ if(response.status){ response["statusCode"]=response.status }elseif(response.statusCode){ response["status"]=response.statusCode } } returnresponse } this.read=(key)=>{ if(isQuanX)return$prefs.valueForKey(key) if(isSurge)return$persistentStore.read(key) } this.notify=(title,subtitle,message)=>{ if(isQuanX)$notify(title,subtitle,message) if(isSurge)$notification.post(title,subtitle,message) if(isNode)console.log(`${title}\n${subtitle}\n${message}`) } this.post=(options,callback)=>{ options.headers['User-Agent']='User-Agent:Mozilla/5.0(iPhone;CPUiPhoneOS13_6_1likeMacOSX)AppleWebKit/609.3.5.0.2(KHTML,likeGecko)Mobile/17G80BiliApp/822mobi_app/ios_comicchannel/AppStoreBiliComic/822' if(isQuanX){ if(typeofoptions=="string")options={ url:options } options["method"]="POST" $task.fetch(options).then(response=>{ callback(null,adapterStatus(response),response.body) },reason=>callback(reason.error,null,null)) } if(isSurge){ options.headers['X-Surge-Skip-Scripting']=false $httpClient.post(options,(error,response,body)=>{ callback(error,adapterStatus(response),body) }) } if(isNode){ node.request.post(options,(error,response,body)=>{ callback(error,adapterStatus(response),body) }) } } this.done=()=>{ if(isQuanX||isSurge){ $done() } }}; 全局变量写好兼容函数后我们先定义一些全局变量,供所有函数调用. 全局变量1234567891011121314151617//新建一个实例对象,把兼容函数定义到$中,以便统一调用let$=newnobyda();//读取兑换商品名,默认兑换积分商城中的"积分兑换";该接口为BoxJs预留,以便修改letproductName=$.read('BM_ProductName')||'积分兑换';//读取兑换数量,默认兑换最大值;该接口为BoxJs预留,以便修改letproductNum=$.read('BM_ProductNum');//读取循环抢购次数,默认100次;该接口为BoxJs预留,以便修改letexchangeNum=$.read('BM_ExchangeNum')||'100';//读取哔哩哔哩漫画签到脚本所使用的Cookieletcookie=$.read('CookieBM');//预留的空对象,便于函数之间读取数据letuser={}; 查询积分脚本开始时首先要做的是查询账号里的积分是否符合兑换要求. 该函数调用时,将执行查询积分,如果查询用户积分成功,则将查询到的积分赋值到全局变量中,供其他函数读取. 查询积分函数1234567891011121314151617181920212223242526272829functionGetUserPoint(){ constpointUrl={//查询积分接口 url:'https://manga.bilibili.com/twirp/pointshop.v1.Pointshop/GetUserPoint', headers:{//请求头 'Cookie':cookie//用户鉴权Cookie } } returnnewPromise((resolve)=>{//主函数返回Promise实例对象,以便后续调用时可以实现顺序执行异步函数 $.post(pointUrl,(error,resp,data)=>{//使用post请求查询,再使用回调函数处理返回的结果 try{//使用try方法捕获可能出现的代码异常 if(error){ thrownewError(error);//如果请求失败,例如无法联网,则抛出一个异常 }else{ constbody=JSON.parse(data);//解析响应体json并转化为对象 if(body.code==0&&body.data){//如果响应体为预期格式 user.point=parseInt(body.data.point);//把查询的积分赋值到全局变量user中 console.log(`\n当前积分:${body.data.point}`);//打印日志 }else{//否则抛出一个异常 thrownewError(body.msg||data); } } }catch(e){//接住try代码块中抛出的异常,并打印日志 console.log(`\n查询积分:失败\n出现错误:${e.message}`); }finally{//finally语句在try和catch之后无论有无异常都会执行 resolve();//异步操作成功时调用,将Promise对象的状态标记为"成功",表示已完成查询积分 } }) })} 查询商品根据前面的分析,我们兑换商品时需要相应的商品ID. 该函数调用时,将执行查询操作,并根据全局变量所定义的商品名进行过滤,过滤后的内容仅包含相关商品的基本信息,例如商品ID、兑换价格、库存等,再把过滤后的内容赋值到全局变量中,供其他函数读取. 查询商品函数1234567891011121314151617181920212223242526272829303132functionListProduct(){ constlistUrl={//查询商品接口 url:'https://manga.bilibili.com/twirp/pointshop.v1.Pointshop/ListProduct', headers:{} } returnnewPromise((resolve)=>{//主函数返回Promise实例对象,以便后续调用时可以实现顺序执行异步函数 $.post(listUrl,(error,resp,data)=>{//使用post请求查询,再使用回调函数处理返回的结果 try{//使用try方法捕获可能出现的代码异常 if(error){ thrownewError(error);//如果请求失败,例如无法联网,则抛出一个异常 }else{ constbody=JSON.parse(data);//解析响应体json并转化为对象 if(body.code==0&&body.data.length>=1){//如果接口正常返回商品信息 //按全局变量所填写的商品名进行过滤,并把商品信息赋值到全局变量user中 user.list=body.data.filter(t=>t.title==productName).pop(); if(!user.list){ thrownewError('请检查商品名');//如果填错商品名则抛出一个异常 }else{//否则打印日志 console.log(`\n查询商品:${productName}\n商品库存:${user.list.remain_amount}`) } }else{//否则抛出一个异常 thrownewError('无商品列表'); } } }catch(e){//接住try代码块中抛出的异常并打印日志 console.log(`\n查询商品:${productName}\n出现错误:${e.message}`); }finally{//finally语句在try和catch之后无论有无异常都会执行 resolve();//异步操作成功时调用,将Promise对象的状态标记为"成功",表示已完成查询商品 } }) })} 兑换商品兑换商品分为两个函数,第一个函数调用后,将根据前面查询到的数据进行判断,如果商品有库存并且用户积分大于100,则根据全局变量所定义的循环次数,暴力调用第二个”请求”函数(默认循环100次) 兑换商品函数12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455functionExchangeProduct(){ returnnewPromise(async(resolve)=>{//主函数返回Promise实例对象,以便后续调用时可以实现顺序执行异步函数,该实例函数带有async关键字,表示里面有异步操作,例如可使用await得到异步结果 if(user.list&&user.list.remain_amount&&user.point>=100){//如果商品有库存并且用户积分大于100则进行抢购 //兑换商品数量(用户积分除与商品单价得到兑换数量),并转成整数;默认兑换最大数量 constnum=parseInt(productNum||(user.point/user.list.real_cost)); constexchangeUrl={ url:'https://manga.bilibili.com/twirp/pointshop.v1.Pointshop/Exchange',//兑换商品接口 headers:{//请求头 'Content-Type':'application/json',//声明请求体数据格式 'Cookie':cookie//用户鉴权Cookie }, body:JSON.stringify({//请求体转成字符串类型 product_id:user.list.id,//兑换的商品id product_num:num,//兑换的商品数量 point:num*user.list.real_cost//消耗的积分总数(兑换数量乘单价得到积分总数) }) }; for(leti=0;i{//主函数返回Promise实例对象,以便后续调用时可以实现顺序执行异步函数 $.post(url,(error,resp,data)=>{//使用post请求查询,再使用回调函数处理返回的结果 try{//使用try方法捕获可能出现的代码异常 if(error){ thrownewError(error);//如果请求失败,例如无法联网,则抛出一个异常 }else{ constbody=JSON.parse(data);//解析响应体json并转化为对象 if(body.code==0){//如果抢购成功,则输出日志和通知 console.log(`\n抢购成功:第${item+1}次\n抢购数量:${amount}\n消耗积分:${amount*user.list.real_cost}`); $.notify('哔哩哔哩漫画抢券','',`"${productName}"抢购成功,数量:${amount},消耗积分:${amount*user.list.real_cost}`); resolve(true);//将Promise对象的状态标记为"成功",然后返回一个布尔值true用于跳出循环 }else{ thrownewError(body.msg||'未知');//抢购失败则抛出异常 } } }catch(e){//接住try代码块中抛出的异常并打印日志 console.log(`\n抢购失败:第${item+1}次\n失败原因:${e.message}`); resolve();//将Promise对象的状态标记为"成功",但不返回任何值,表示继续循环抢购 } }) })} 统一调用写好函数后,如果不进行调用,那么代码是无法被运行的. 在javascript中,代码的执行顺序很重要,我们的需求是先同时查询积分和商品,再抢购,最后退出脚本. 以下匿名函数将按我们预期的顺序执行. 匿名异步函数123456789(asyncfunction(){//立即运行的匿名异步函数 //使用await关键字声明,表示以同步方式执行异步函数,可以简单理解为顺序执行 awaitPromise.all([//该方法用于将多个实例包装成一个新的实例,可以简单理解为同时调用函数,以进一步提高执行速度 GetUserPoint(),//查询积分函数 ListProduct()//查询商品函数 ]); awaitExchangeProduct();//上面的查询都完成后,则执行抢购 $.done();//抢购完成后调用Surge、QX内部特有的函数,用于退出脚本执行})(); 第四步:配置任务我们写好脚本后,就可以在Surge或QuantumultX里配置定时任务让它在规定时间内执行该脚本. 以下将用Surge进行演示 编辑Surge配置文件,在[Script]段落放入以下脚本 1哔哩哔哩漫画抢券=type=cron,cronexp="10,20,30012***",wake-system=1,timeout=60,script-path=https://raw.githubusercontent.com/NobyDa/Script/master/Bilibili-DailyBonus/ExchangePoints.js 因为积分商城刷新时间在每天中午12点,则以上配置将在每天中午12:00:10、12:00:20、12:00:30分别执行一次. 有几点需要注意 脚本抢购完成后注意禁用,避免每天无意义的运行 脚本需要使用哔哩哔哩签到脚本获取Cookie后方可使用. 默认兑换积分商城中的“积分兑换”;可自行修改. 兑换数量为用户积分可兑换的最大值;可自行修改. 结语本文又是一篇专业性极强的文章,有的同学可能又要头大了;写这篇文章动机主要是为了让读者了解一个需求的实现过程,脚本里也有大量注释供读者理解思路. 这个需求说难也不太难,就是抓几个接口去模拟用户兑换的行为.当然还是需要有一定的抓包经验和javascript功底才行;我的javascript功底也就勉勉强强够我写几个简单需求. 虽然本文看起来比较水,但还是建议初学者照着本文去理解抓包思路,以及脚本的执行逻辑. 最后,提前祝大家周末愉快! 记录一次Surge&QuantumultX脚本开发过程https://nobyda.github.io/2021/07/16/BilibiliManga_Js_example/作者NobyDa发布于2021-07-16更新于2021-07-17许可协议#SurgeQuantumultXJavaScript喜欢这篇文章?打赏一下作者吧微信巴哈姆特自动签到脚本(适配/开发实例)使用Surge脚本&逻辑规则建立联网防火墙评论×



請為這篇文章評分?