全部 / 技术交流 · 2023年1月11日 0

Steam游戏信息查询程序设计(node.js)

本文最后更新于 693 天前,其中的信息可能已经有所发展或改变。

中文检索

在以上的代码中,已经实现了基本的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

使用翻译软件来搜索游戏译名始终存在限制,即使是bing翻译也只能翻译一些热门游戏,涵盖的范围相当少,想让中文检索正常使用,必须进一步提升准确率。

vgtime

在日常生活中,我发现国内的游戏自媒体网站都附带了游戏库功能,用户无论是搜索中文还是英文都能准确找到自己需要的游戏,有没有一种方法可以使用这些媒体的搜索功能,找到正确的英文译名呢?

在我的手机上,常用的游戏资讯app包括二柄、小黑盒、vgtime、a9vg,对它们的搜索功能逐一进行抓包测试。

Stream抓包

对于这些应用的抓包,我希望满足:

  • 搜索时可以同时列出游戏的英文名与中文名
  • 调用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;
    }
Java

sign方法使用了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的搜索框,准确率进一步提高!

文章分页:上一页 1 2 3 下一页
页面: 1 2 3