Appearance
API
下文描述的是游戏客户端直连官方服时的协议与习惯:入口地址、URL 公参、POST 正文与签名的编解码。文中 「与 tomato.ts 一致」 的部分与抓包/对照实现 tomato.ts 中的常量与函数一一对应(便于核对 MD5 与 Base64 层级)。
入口路径
各环境下的 主机 见下表;游戏服接口路径一般为 /servers/api.php(与 tomato.ts 里 API_URL 的 pathname 一致)。完整 URL 形如:
text
https://<BASE表中的主机>/servers/api.php?<编码后的查询串>请求方法为 POST;正文为编码后的字符串(见下文),Content-Type 以客户端为准(常见为 text/plain 或表单类,以抓包为准)。
不同环境下的 BASE URL
| 代码标识 | BASE URL |
|---|---|
| ConfigFinalFormal | https://store-game-van2.tomatogames.com |
| ConfigFinalV2 | https://store-game-van2.tomatogames.com |
| ConfigFinal | https://store-game.tomatogames.com |
| ConfigInner | http://192.168.1.191:1080 |
| ConfigOnlineTest | http://14.103.43.115:8010 |
| ConfigPreonline | http://14.103.43.115:2080 |
| ConfigTTMiniTest | https://store-mini-release2.tomatogames.com |
| ConfigTTMiniFormal | https://store-mini-douyints.tomatogames.com |
| ConfigTTMini | https://store-game-formal.tomatogames.com |
| ConfigTestNoSdk | http://14.103.43.115:8020 |
| ConfigTest | http://14.103.43.115:8020 |
| ConfigTishen | http://101.43.54.37 |
| ConfigWXMiniTest | https://store-mini-release2.tomatogames.com |
| ConfigWXMiniVerify | https://store-mini-wechatts.tomatogames.com |
| ConfigWXMini | https://store-game-formal.tomatogames.com |
常量(与 tomato.ts 一致)
| 符号 | 值 | 含义 |
|---|---|---|
signSalt | _L76CC1239C62F9CB39E2085848A7427B | 参与计算 sg 时在业务体后拼接的固定盐。 |
parseFix | 194a0291323274a254e86e914acbafe1 | 32 位十六进制串;参与查询串与正文的双层 Base64封装,并用于识别响应的简式/繁式解析。 |
POST 正文与查询串的编码(与 tomato.ts 一致)
以下记 body 为业务 JSON 对象(如 { "well": { "info": { ... } } }),param 为与 URL 公参一一对应的扁平字符串键值(Record<string, string>),在计算签名前尚未带 sg。
1. 计算 sg 并写入查询参数
与 parseParamWithSign 一致:
- 将业务体序列化为字符串:
payload = JSON.stringify(body)(键顺序、是否含空格会影响 MD5,须与客户端发包前用于算签的字符串一致)。 sg = MD5( payload + signSalt )(MD5 输出为小写十六进制字符串,与md5包默认行为一致)。- 将
sg与其它公参一并放入URLSearchParams,得到明文查询串queryPlain(key=value&...)。
客户端旧脚本里常见写法是对 POST 正文的原始字符串 (e || "") 做 MD5(e + salt);若该字符串与 JSON.stringify(body) 逐字符相同,则与上式等价。
2. 查询串在 URL 上的最终形态
与 parseParamWithSign 末尾一致(记 btoa 为 Base64 编码):
text
queryWire = btoa( btoa(queryPlain) + parseFix )浏览器/Node 中 btoa 按 UTF-8 字节 处理 ASCII 段;若公参中含非 ASCII,需与运行环境一致(必要时用 encodeURIComponent 再入参,以抓包为准)。
最终请求 URL 为:
text
<scheme>://<host>/servers/api.php? + queryWire即 ? 后只有一段经双层 Base64 与后缀拼接后的密文(不再保留 sevid=... 可读形态);公参信息已包含在 queryPlain 的内层之中。
3. POST 正文(body)的最终形态
与 parseBodyWithSign 一致:
text
bodyWire = btoa( btoa( JSON.stringify(body) ) + parseFix )HTTP POST 的 body 即为字符串 bodyWire(不再发送明文 JSON)。
4. 小结:双层结构
| 方向 | 内层 | 外层 |
|---|---|---|
| Query | btoa(queryPlain) | btoa(内层 + parseFix) |
| Body | btoa(JSON.stringify(body)) | btoa(内层 + parseFix) |
parseFix 始终接在第一层 Base64 结果之后、第二层 Base64 之前。
响应体解码(与 parseReturnBody 一致)
记响应正文为字符串 raw(与 axios 拿到的 response.data 类型一致时先转成字符串)。
- 简式:若
raw的最后 32 个字符等于parseFix,则去掉这 32 位后,剩余部分直接JSON.parse得到业务对象。 - 繁式:否则
step1 = atob(raw)- 去掉
step1末尾 32 字符 - 对剩余串再
atob得到 JSON 文本,JSON.parse得到业务对象。
客户端在「加密」模式下若对响应再做 decode,需与上述分支一致,否则会解析失败。
API URL 结构(明文公参,未做 queryWire 时)
基本参数
这里是除签名以外的基本参数
javascript
t.prototype.getUrl =
function () {
return s.default.url+"?sevid="+s.default.serId+"&ver="+s.default.version+"&uid="+s.default.uid+"&token="+s.default.token+"&platform="+s.default.pf+"&lang="+s.default.lang+"&cid="+s.default.cid+"&mini_mark="+window.mini_mark
}签名
签名参数 sg 是通过以下方式计算得到的
javascript
var r=t,a=t.indexOf("?"),l="";
a>=0&&(r=t.slice(0,a),l=t.slice(a+1));
var c=(l=l+(l?"&":"")+"sg="+MD5.md5((e||"")+"_L76CC1239C62F9CB39E2085848A7427B"))?this.encode(l):"";
t=r+"?"+c通过分析,我们可以得出签名参数 sg 是通过对请求主体(e)加上一个固定字符串 "_L76CC1239C62F9CB39E2085848A7427B" 进行 MD5 哈希计算得到的。r和l 分别是 URL 的路径和查询参数部分,最终的 URL 是将路径和带有签名的查询参数拼接起来形成的。
与「POST 正文与查询串的编码」对齐:旧代码里的
this.encode(signedQuery)、this.encode(body)在「全链路加密」时应等价于tomato.ts中的parseParamWithSign/parseBodyWithSign(双层 Base64 +parseFix)。若只对 query 做单次 Base64,或参与 MD5 的字符串与JSON.stringify(业务对象)不完全一致,则与正式服抓包可能对不上。
通过优化代码,我们可以更清晰地理解 URL 的结构:
javascript
const url = t
const i = url.indexOf("?")
let path = url
let query = ""
if (i !== -1) {
path = url.slice(0, i)
query = url.slice(i + 1)
}
const signature = MD5.md5((e || "") + "_L76CC1239C62F9CB39E2085848A7427B")
const signedQuery = query + (query ? "&" : "") + "sg=" + signature
const finalUrl = path + "?" + this.encode(signedQuery)javascript
this.send=function(t,e){
try{if(this.Config&&!this.Config.token)return;var o=null!=t?t.getJson():"";if(0==s&&a==o)return;this.isAdok(o)||(a=o),this.httpRequest(this.getUrl(),o,e)}catch(i){console.log(i.toString())}},this.sendLast=function(){try{this.httpRequest(this.getUrl(),a)}catch(t){console.log(t.toString())}},
this.httpRequest=function(t,e,o){var n=null!=e&&0==this.isAdok(e);if(facade&&facade.send("DEAL_WAIT_UI",{isTttpWait:!0,value:n}),i.default.playerProxy.getIsEncryption()){var r=t,a=t.indexOf("?"),l="";a>=0&&(r=t.slice(0,a),l=t.slice(a+1));var c=(l=l+(l?"&":"")+"sg="+MD5.md5((e||"")+"_L76CC1239C62F9CB39E2085848A7427B"))?this.encode(l):"";t=r+"?"+c}var p=cc.loader.getXMLHttpRequest();p.timeout=3e4,p.onloadend=function(){if(s=!0,null==p||null==p.status){cc.warn(t+" request is error: status is null"),facade&&facade.send("GAME_LINK_TIPS",1);var e={a:{}};e.a.system={},e.a.system.errror="http error status null",null!=o&&o(e)}else if(200==p.status){if(facade&&facade.send("DEAL_WAIT_UI",{isTttpWait:!0,value:!1}),null==p.response||""==p.response||" "==p.response){cc.warn(t+" request is error: response empty"),facade&&facade.send("GAME_LINK_TIPS",2);var n={a:{}};n.a.system={},n.a.system.errror="http error status 200",null!=o&&o(n)}else{var r=p.response;if(i.default.playerProxy.getIsEncryption()&&(r=this.decode(p.response)),null!=r&&0==r.indexOf("{")){var a=JSON.parse(r);this._dealSub(a),null!=o&&o(a)}else{cc.warn(t+" request is error response is error:"+r),facade&&facade.send("GAME_LINK_TIPS",3);var l={a:{}};l.a.system={},l.a.system.errror="http error status 200 response is error",null!=o&&o(l)}}}else{cc.warn(t+" request is error status:"+p.status),facade&&facade.send("GAME_LINK_TIPS",4);var c={a:{}};c.a.system={},c.a.system.errror="http error status "+p.status,null!=o&&o(c)}}.bind(this),p.ontimeout=function(){cc.warn(t+" request is time out readyState:"+p.readyState)}.bind(this),p.open("POST",t),null!=e&&""!=e?i.default.playerProxy.getIsEncryption()?p.send(this.encode(e)):p.send(e):p.send()}通过重构:
javascript
this.send = function (requestData, callback) {
try {
// 如果配置存在但没有 token,则不发送
if (this.Config && !this.Config.token) return;
const body = requestData != null ? requestData.getJson() : "";
// ...
this.httpRequest(this.getUrl(), body, callback);
} catch (err) {
console.log(err.toString());
}
};
this.httpRequest = function (url, body, callback) {
const shouldShowWait =
body != null && this.isAdok(body) === false;
// 加密模式下,需要给 URL 参数追加签名 sg
if (i.default.playerProxy.getIsEncryption()) {
let baseUrl = url;
const queryIndex = url.indexOf("?");
let query = "";
if (queryIndex >= 0) {
baseUrl = url.slice(0, queryIndex);
query = url.slice(queryIndex + 1);
}
const sign = MD5.md5(
(body || "") + "_L76CC1239C62F9CB39E2085848A7427B"
);
query = query + (query ? "&" : "") + "sg=" + sign;
const encodedQuery = query ? this.encode(query) : "";
url = baseUrl + "?" + encodedQuery;
}
const xhr = cc.loader.getXMLHttpRequest();
xhr.timeout = 30000;
xhr.open("POST", url);
if (body != null && body !== "") {
if (i.default.playerProxy.getIsEncryption()) {
xhr.send(this.encode(body));
} else {
xhr.send(body);
}
} else {
xhr.send();
}
};我们需要知道 requestData.getJson() 的返回值是什么样的,因此,翻查源码找到:
javascript
var i=function(t){var e=function(){this.getJson=function(){var e=t.split(".");return"{\""+e[1]+"\":{\""+e[2]+"\":"+JSON.stringify(this)+"}}"}};return e.key=t,e.class=t,e},
n={well:{info:i("proto_sc.well.info"),news:i("proto_sc.well.news"),museum_list:i("proto_sc.well.museum_list"),kua_list:i("proto_sc.well.kua_list")}}所以我们可以推测出,假设传入 proto_sc.well.info,requestData.getJson() 的返回值是一个 JSON 字符串,格式如下:
json
{
"well": {
"info": { ... },
}
}搜索 getJson 得到:
javascript
function(){var e=t.split(".");return"{\""+e[1]+"\":{\""+e[2]+"\":"+JSON.stringify(this)+"}}"}URL 参数说明(推测)
| 参数名 | 说明 |
|---|---|
| sevid | 服务 ID |
| ver | 版本号 |
| uid | 用户 ID |
| token | 用户身份验证 token |
| platform | 平台标识 |
| lang | 语言 |
| cid | 客户端 ID |
| mini_mark | 小程序标识 |