19x
19x
2x
17x
15x
15x
2x
13x
1x
12x
1x
11x
11x
11x
11x
11x
11x
11x
11x
11x
11x
11x
24x
24x
4x
11x
36x
36x
36x
36x
11x
11x
11x
1x
1x
11x
11x
11x
11x
11x
25x
25x
25x
25x
25x
25x
25x
25x
25x
1x
24x
24x
11x
49x
49x
49x
49x
|
import { SignResult } from '../model/SignResult';
/**
* `qianapp://` 协议解析器(纯函数,客户也可在 SDK 容器外直接调用)。
*
* **解析约定**:
* - host + path 拼接为 `customPath`(去除前后斜杠)
* - query 按 `application/x-www-form-urlencoded` 语义 UTF-8 解码,`+` 等价空格
* - 重复 key 保留**首个**,空 value(`?action=`)保留为空字符串
* - 已知字段(`flowId` / `action` / `result` / `from`)抽出到对应字段;未知字段透传到 `extras`
* - action / result 未识别值原样透出(SDK 不做收敛)
* - 非法 URL / scheme 不匹配 / 长度超过 8KB → 返回 `SignResult.EMPTY`
*/
export class SignResultParser {
private static readonly SCHEME: string = 'qianapp';
private static readonly SCHEME_PREFIX: string = 'qianapp://';
/** URL 最大长度限制(8KB),超过视为恶意构造,返回 SignResult.EMPTY 并丢弃。 */
private static readonly MAX_URL_LENGTH: number = 8 * 1024;
private static readonly RECOGNIZED_KEYS: string[] = ['flowId', 'action', 'result', 'from'];
/**
* 判断给定 URL 是否为本 SDK 关心的 `qianapp://` scheme(大小写不敏感)。
*/
public static isQianAppScheme(url: string | null | undefined): boolean {
if (url === null || url === undefined || url.length === 0) {
return false;
}
return url.toLowerCase().startsWith(SignResultParser.SCHEME_PREFIX);
}
/**
* 解析 `qianapp://...` URL。
*
* 失败时返回 SignResult.EMPTY。
*
* @param url 完整 URL,例 `qianapp://callback/sign?flowId=abc&action=sign&result=success&from=tencent_ess`
*/
public static parse(url: string | null | undefined): SignResult {
if (url === null || url === undefined || url.length === 0) {
return SignResult.EMPTY;
}
if (!SignResultParser.isQianAppScheme(url)) {
return SignResult.EMPTY;
}
if (url.length > SignResultParser.MAX_URL_LENGTH) {
return SignResult.EMPTY;
}
// 拆 scheme://body[?query]
const body: string = url.substring(SignResultParser.SCHEME_PREFIX.length);
const splitBody: [string, string] = SignResultParser.splitOnce(body, '?');
const pathPart: string = splitBody[0];
const queryPart: string = splitBody[1];
// host + path 即为 customPath, 去除前后斜杠
const customPath: string = SignResultParser.trimSlashes(pathPart);
// 解析 query
const parsed: Map<string, string> = SignResultParser.parseQuery(queryPart);
// 抽出 recognized fields
const flowId: string = parsed.has('flowId') ? (parsed.get('flowId') as string) : '';
const action: string = parsed.has('action') ? (parsed.get('action') as string) : '';
const result: string = parsed.has('result') ? (parsed.get('result') as string) : '';
const from: string = parsed.has('from') ? (parsed.get('from') as string) : '';
// 剩余 query 落入 extras
const extras: Map<string, string> = new Map<string, string>();
parsed.forEach((value: string, key: string) => {
if (SignResultParser.RECOGNIZED_KEYS.indexOf(key) < 0) {
extras.set(key, value);
}
});
return new SignResult(url, customPath, flowId, action, result, from, extras);
}
/** 在第一个 [delimiter] 上拆分,返回 [左, 右]。无分隔符时右半为空字符串。 */
private static splitOnce(input: string, delimiter: string): [string, string] {
const idx: number = input.indexOf(delimiter);
if (idx < 0) {
return [input, ''];
}
return [input.substring(0, idx), input.substring(idx + 1)];
}
/** 去除字符串前后的 `/`。 */
private static trimSlashes(input: string): string {
let start: number = 0;
let end: number = input.length;
while (start < end && input.charAt(start) === '/') {
start++;
}
while (end > start && input.charAt(end - 1) === '/') {
end--;
}
return input.substring(start, end);
}
/**
* 解析 query 字符串。
*
* 规则: 以 `&` 拆 pair,每个 pair 在第一个 `=` 处拆 key/value,
* key 与 value 都做 UTF-8 percent-decode,`+` 等价空格;
* 空 key 跳过,空 value 保留;重复 key 保留首个;decode 失败的 pair 跳过。
*/
private static parseQuery(query: string): Map<string, string> {
const out: Map<string, string> = new Map<string, string>();
if (query.length === 0) {
return out;
}
const pairs: string[] = query.split('&');
for (let i: number = 0; i < pairs.length; i++) {
const pair: string = pairs[i];
if (pair.length === 0) {
continue;
}
const split: [string, string] = SignResultParser.splitOnce(pair, '=');
const rawKey: string = split[0];
const rawValue: string = split[1];
const key: string | null = SignResultParser.decodeOrNull(rawKey);
if (key === null || key.length === 0) {
continue;
}
// 重复 key 保留首个
if (out.has(key)) {
continue;
}
const value: string | null = SignResultParser.decodeOrNull(rawValue);
out.set(key, value === null ? '' : value);
}
return out;
}
/**
* UTF-8 percent-decoded; `+` 等价空格(form-urlencoded)。
* 解码失败返回 null。
*/
private static decodeOrNull(raw: string): string | null {
try {
// 先把 `+` 替换为 `%20`(application/x-www-form-urlencoded 语义),再 decodeURIComponent
const replaced: string = raw.replace(/\+/g, '%20');
return decodeURIComponent(replaced);
} catch (_e) {
return null;
}
}
}
|