中文检索
在以上的代码中,已经实现了基本的Steam库查询功能,但只能接受英文输入,有什么办法进行中文检索呢?
自动翻译
首个思路就是进行翻译,koishi框架中许多插件可以作为服务,程序可以直接调用翻译服务。
if (config.translate) {
if (text.match(/[\u4e00-\u9fa5]+/g)) {
try {
// 将中文翻译成英文
text = (await ctx.translator.translate({ input: text, target: 'en' })).toLocaleLowerCase();
translated = true;
} catch (err) {
console.warn(err);
}
}
}else{
if (text.match(/[\u4e00-\u9fa5]+/g) && !config.bing && !config.Chinese_support) {
return `检测到输入包含中文,并且未启用自动翻译或中文匹配,检索失败!`
}
}
JavaScript使用ctx.translator.translate进行翻译,并且附有报错排查。但koishi的翻译服务仅有百度翻译、有道翻译、人人有翻译三个插件,实际测试中没有一个游戏译名可以准确翻译,即使输入了中文也查询不到任何信息。
Bing翻译
问题很明显,需要一个更为准确的翻译,在我的测试下,国外公司提供的翻译服务似乎更为准确。在Deepl、谷歌翻译与bing翻译中,我最终选择了bing翻译,因为只有后者在申请API时,没有信用卡限制,按照微软的官方文档,写出代码调用API:
if (config.bing) {
if (text.match(/[\u4e00-\u9fa5]+/g)) {
try {
// 将中文翻译成英文
let key = config.bing_secret;
let region = config.bing_region;
let endpoint = "https://api.cognitive.microsofttranslator.com";
const response = await axios({
baseURL: endpoint,
url: '/translate',
method: 'post',
headers: {
'Ocp-Apim-Subscription-Key': key,
'Content-type': 'application/json',
'Ocp-Apim-Subscription-Region': region,
'X-ClientTraceId': uuidv4().toString()
},
params: {
'api-version': '3.0',
'from': 'zh-CN',
'to': 'en'
},
data: [{
'text': text
}],
responseType: 'json'
});
text = response.data[0].translations[0].text;
translated = true;
} catch (err) {
console.warn(err);
}
}
} else if (text.match(/[\u4e00-\u9fa5]+/g) && !config.translate && !config.Chinese_support) {
return `检测到输入包含中文,并且未启用翻译或中文匹配,检索失败!`;
}
JavaScript白嫖Vgtime的检索功能
使用翻译软件来搜索游戏译名始终存在限制,即使是bing翻译也只能翻译一些热门游戏,涵盖的范围相当少,想让中文检索正常使用,必须进一步提升准确率。
在日常生活中,我发现国内的游戏自媒体网站都附带了游戏库功能,用户无论是搜索中文还是英文都能准确找到自己需要的游戏,有没有一种方法可以使用这些媒体的搜索功能,找到正确的英文译名呢?
在我的手机上,常用的游戏资讯app包括二柄、小黑盒、vgtime、a9vg,对它们的搜索功能逐一进行抓包测试。
对于这些应用的抓包,我希望满足:
- 搜索时可以同时列出游戏的英文名与中文名
- 调用API时不需要使用Cookie
- 可以支持http传输、参数未经加密
最先想到的是Vgtime App,因为在我的印象中,Vgtime的代码算是比较老旧的,其安全措施可能并不到位,因此可能是个很好地切入点。
果然连SSL证书都没加上,直接在浏览器里输入请求地址。
显然是不行的,加上请求头部,返回结果中同时包含了中英文名称,相当成功!
请求参数为
__channel=App%20Store&__device_id=F0C40669-866A-4579-BCE8-6CFD8FB67370&__device_type=iPhone14%2C2&__network=Wifi&__os_version=16.3&__platform=iOS&__resolution=1170%2A2532&__s_version=0.0.1&__version=2.9.16&appId=1bc2a63f60f64ff0bd0815f78933b940&keyword=oneshot&page=1&page_size=20&sign=ec102e731a4f4d61cef2b1dae505f2d6c11c4887&type=3
JSON似乎不包含用户的token,无须登陆即可使用,只需改变keyword参数就可以实现游戏检索。真的吗?
改变keyword后,返回的结果如下
{"message":"sign验证失败","retcode":101}
JSON这里可以看到,这个链接使用了sign参数作为服务器与客户端的验证密钥,抓包取得的sign值为“ec102e731a4f4d61cef2b1dae505f2d6c11c4887”。它是怎么得出的?观察其长度似乎也不符合md5加密,一个小时后使用相同的参数发起post请求,仍然成功,说明sign值也并非是对时间的简单加密,似乎只与请求的内容有关。做到这一步,这个方法几乎可以判定为无法成功,但我还是想尝试下找出sign值的计算方法。
尝试反编译
下载vgtime apk丢入apktools,发现未加固。
打开jadx进行反编译
package anet.channel.security;
import android.content.Context;
/* compiled from: Taobao */
/* loaded from: classes.dex */
public interface ISecurity {
public static final String CIPHER_ALGORITHM_AES128 = "ASE128";
public static final String SIGN_ALGORITHM_HMAC_SHA1 = "HMAC_SHA1";
byte[] decrypt(Context context, String str, String str2, byte[] bArr);
byte[] getBytes(Context context, String str);
boolean isSecOff();
boolean saveBytes(Context context, String str, byte[] bArr);
String sign(Context context, String str, String str2, String str3);
}
Java可以看出字符串sign使用的加密算法为 AES128 、签名算法为 HMAC_SHA1。
sign(Context context, String str, String str2, String str3)方法的参数含义分别为:
- context: 上下文对象
- str: 签名算法
- str2: appkey
- str3: 要签名的字符串
class d {
public static final String TAG = "amdc.DispatchParamBuilder";
public static Map a(Map<String, Object> map) {
IAmdcSign sign = AmdcRuntimeInfo.getSign();
if (sign != null && !TextUtils.isEmpty(sign.getAppkey())) {
NetworkStatusHelper.NetworkStatus status = NetworkStatusHelper.getStatus();
if (!NetworkStatusHelper.isConnected()) {
ALog.e(TAG, "no network, don't send amdc request", null, new Object[0]);
return null;
}
map.put("appkey", sign.getAppkey());
map.put("v", DispatchConstants.VER_CODE);
map.put("platform", "android");
map.put(DispatchConstants.PLATFORM_VERSION, Build.VERSION.RELEASE);
if (!TextUtils.isEmpty(GlobalAppRuntimeInfo.getUserId())) {
map.put("sid", GlobalAppRuntimeInfo.getUserId());
}
map.put(DispatchConstants.NET_TYPE, status.toString());
map.put("carrier", NetworkStatusHelper.getCarrier());
map.put(DispatchConstants.MNC, NetworkStatusHelper.getSimOp());
if (AmdcRuntimeInfo.latitude != 0.0d) {
map.put("lat", String.valueOf(AmdcRuntimeInfo.latitude));
}
if (AmdcRuntimeInfo.longitude != 0.0d) {
map.put("lng", String.valueOf(AmdcRuntimeInfo.longitude));
}
map.putAll(AmdcRuntimeInfo.getParams());
map.put("channel", AmdcRuntimeInfo.appChannel);
map.put("appName", AmdcRuntimeInfo.appName);
map.put("appVersion", AmdcRuntimeInfo.appVersion);
map.put(DispatchConstants.STACK_TYPE, Integer.toString(a()));
map.put(DispatchConstants.DOMAIN, b(map));
map.put(DispatchConstants.SIGNTYPE, sign.useSecurityGuard() ? com.taobao.accs.antibrush.b.KEY_SEC : "noSec");
map.put("t", String.valueOf(System.currentTimeMillis()));
String a10 = a(sign, map);
if (TextUtils.isEmpty(a10)) {
return null;
}
map.put("sign", a10);
return map;
}
ALog.e(TAG, "amdc sign is null or appkey is empty", null, new Object[0]);
return null;
}
Java这段处理请求参数的类中,可以看到调用 IAmdcSign.sign(String str) 方法,但没有给出具体代码,继续搜索IAmdcSign。
public class d implements IAmdcSign {
/* renamed from: a reason: collision with root package name */
public final /* synthetic */ String f4529a;
/* renamed from: b reason: collision with root package name */
public final /* synthetic */ ISecurity f4530b;
/* renamed from: c reason: collision with root package name */
public final /* synthetic */ SessionCenter f4531c;
public d(SessionCenter sessionCenter, String str, ISecurity iSecurity) {
this.f4531c = sessionCenter;
this.f4529a = str;
this.f4530b = iSecurity;
}
@Override // anet.channel.strategy.dispatch.IAmdcSign
public String getAppkey() {
return this.f4529a;
}
@Override // anet.channel.strategy.dispatch.IAmdcSign
public String sign(String str) {
return this.f4530b.sign(this.f4531c.f4470b, ISecurity.SIGN_ALGORITHM_HMAC_SHA1, getAppkey(), str);
}
@Override // anet.channel.strategy.dispatch.IAmdcSign
public boolean useSecurityGuard() {
return !this.f4530b.isSecOff();
}
}
Java这一段明确地给出了IAmdcSign接口的实现,并且通过f4530b字段调用sign方法进行签名,用于签名算法为 HMAC_SHA1,我们知道了签名值的由来,但仍然不知道签名的计算方法。
f4530b字段来自接口ISecurity,继续搜索相关类的实现:
public String sign(Context context, String str, String str2, String str3) {
if (!TextUtils.isEmpty(this.f4716a) && ISecurity.SIGN_ALGORITHM_HMAC_SHA1.equalsIgnoreCase(str)) {
return HMacUtil.hmacSha1Hex(this.f4716a.getBytes(), str3.getBytes());
}
return null;
}
static {
try {
Class.forName("com.alibaba.wireless.security.open.SecurityGuardManager");
f4718b = true;
HashMap hashMap = new HashMap();
f4719c = hashMap;
hashMap.put(ISecurity.SIGN_ALGORITHM_HMAC_SHA1, 3);
f4719c.put(ISecurity.CIPHER_ALGORITHM_AES128, 16);
} catch (Throwable unused) {
f4718b = false;
}
}
public b(String str) {
this.f4720d = null;
this.f4720d = str;
}
Javasign方法使用了HMacUtil.hmacSha1Hex来对传入的str3进行签名,并使用了一个类级别的字符串变量f4716a来作为签名秘钥,只支持签名算法为"SIGN_ALGORITHM_HMAC_SHA1"。ISecurity.sign() 方法是通过 HMacUtil.hmacSha1Hex(this.f4716a.getBytes(), str3.getBytes()) 来计算 sign 值的。其中 this.f4716a 是秘钥,str3 是要签名的字符串。
大致明白了sign值的算法实现,但我并没有成功,加上代码中时不时出现的阿里安全组件,破解它想必相当困难,在这一条路上一直耗下去肯定行不通。
引入Cherrio库解析HTML
既然app内部的接口没法破解,接下来就尝试爬取HTML代码,使用cheerio库进行解析。
const cheerio = require('cheerio');
JavaScript进入Vgtime官网,F12检查搜索界面的html,可以发现界面的Html还是比较简单的
定义两个新的函数分别获取搜索结果的英文名称和中文名称。
function extractGameNames(html) {
const $ = cheerio.load(html);
const gameNames = [];
$('.game').each((i, element) => {
const $element = $(element);
const gameName = $element.find('h2 a').text();
gameNames.push(gameName);
});
return gameNames[0];
}
function extractEnglishGameNames(html) {
const $ = cheerio.load(html);
const gameNames = [];
$('.game').each((i, element) => {
const $element = $(element);
const gameName = $element.find('.old_name').text();
gameNames.push(gameName);
});
return gameNames[0];
}
JavaScript在主函数中引用它
if (config.Chinese_support) {
if (text.match(/[\u4e00-\u9fa5]+/g)) {
try {
const url = `https://www.vgtime.com/search/game.jhtml?keyword=${text}&type=game&page=1&pageSize=12&domName=search_game_list`;
const response = await axios.get(url);
const html = response.data;
// 调用提取游戏名称的函数
const gameNames = extractGameNames(html);
const EnglishGameNames = extractEnglishGameNames(html);
if (EnglishGameNames.length === 0 || !/[a-zA-Z]/.test(EnglishGameNames)) {
return 'Sorry, 此游戏不存在英文名称,无法检索!您可以尝试直接查询英文名称。';
}
text = EnglishGameNames.replace(/[^a-zA-Z0-9]/g, ' ');
vgtime = true
} catch (err) {
console.warn(err);
}
}
}else{
if (text.match(/[\u4e00-\u9fa5]+/g) && !config.bing && !config.Chinese_support) {
return `检测到输入包含中文,并且未启用自动翻译或中文匹配,检索失败!您可以尝试直接查询英文名称。`
}
}
JavaScript至此,中文检索白嫖了Vgtime的搜索框,准确率进一步提高!