@nsnanocat/grpc
Version:
Pure JS's gRPC decode/encode module for well-known iOS network tools
125 lines (116 loc) • 4.6 kB
JavaScript
import { $app, Console } from "@nsnanocat/util";
import pako from "pako";
const TEXT_DECODER = new TextDecoder();
/* https://grpc.io/ */
export default class gRPC {
static decode(bytesBody = new Uint8Array([])) {
Console.log("☑️ gRPC.decode");
// 先拆分gRPC校验头和protobuf数据体
const Header = bytesBody.slice(0, 5);
let body = bytesBody.slice(5);
switch (Header[0]) {
case 0: // unGzip
default:
break;
case 1: // Gzip
body = gRPC.#ungzip(body); // 解压缩protobuf数据体
Header[0] = 0; // unGzip
break;
}
Console.log("✅ gRPC.decode");
return body;
}
static decodeWeb(bytesBody = new Uint8Array([])) {
Console.log("☑️ gRPC.decodeWeb");
// 解析 grpc-web binary unary 响应,返回 trailer header 和 protobuf bodyBytes
const header = {};
let bodyBytes = new Uint8Array([]);
let dataFrameCount = 0;
let offset = 0;
while (offset < bytesBody.length) {
// grpc-web frame: 1 byte flag + 4 bytes big-endian length + frame payload
if (offset + 5 > bytesBody.length) throw new Error("Invalid gRPC-Web frame header");
const frameFlag = bytesBody[offset];
const frameLength = gRPC.#ReadUInt32(bytesBody, offset + 1);
offset += 5;
if (offset + frameLength > bytesBody.length) throw new Error("Invalid gRPC-Web frame length");
let frameBody = bytesBody.slice(offset, offset + frameLength);
offset += frameLength;
const isTrailer = (frameFlag & 0x80) === 0x80;
const isCompressed = (frameFlag & 0x01) === 0x01;
if ((frameFlag & 0x7e) !== 0) throw new Error(`Unsupported gRPC-Web frame flag: ${frameFlag}`);
if (isTrailer) {
// unary 响应里 trailer frame 必须是最后一帧,内部是 CRLF 分隔的 header block
if (offset !== bytesBody.length) throw new Error("Invalid gRPC-Web response: trailer frame must be last");
if (isCompressed) frameBody = gRPC.#ungzip(frameBody);
Object.assign(header, gRPC.#parseHeaderBlock(frameBody));
continue;
}
// 当前只支持 unary,因此只接受一个 data frame,并把它解成 protobuf body
if (dataFrameCount > 0) throw new Error("Invalid gRPC-Web unary response: multiple data frames");
dataFrameCount += 1;
bodyBytes = isCompressed ? gRPC.#ungzip(frameBody) : frameBody;
}
Console.log("✅ gRPC.decodeWeb");
return { header, bodyBytes };
}
static encode(body = new Uint8Array([]), encoding = "identity") {
Console.log("☑️ gRPC.encode");
// Header: 1位:是否校验数据 (0或者1) + 4位:校验值(数据长度)
const Header = new Uint8Array(5);
const Checksum = gRPC.#Checksum(body.length); // 校验值为未压缩情况下的数据长度, 不是压缩后的长度
Header.set(Checksum, 1); // 1-4位: 校验值(4位)
switch (encoding) {
case "gzip":
Header.set([1], 0); // 0位:Encoding类型,当为1的时候, app会校验1-4位的校验值是否正确
body = pako.gzip(body);
break;
case "identity":
case undefined:
default:
Header.set([0], 0); // 0位:Encoding类型,当为1的时候, app会校验1-4位的校验值是否正确
break;
}
const BytesBody = new Uint8Array(Header.length + body.length);
BytesBody.set(Header, 0); // 0-4位:gRPC校验头
BytesBody.set(body, 5); // 5-end位:protobuf数据
Console.log("✅ gRPC.encode");
return BytesBody;
}
static #ungzip = (body = new Uint8Array([])) => {
switch ($app) {
case "Loon":
case "Surge":
case "Egern":
return $utils.ungzip(body);
default:
return pako.ungzip(body);
}
};
static #ReadUInt32 = (bytes = new Uint8Array([]), offset = 0) => {
const view = new DataView(bytes.buffer, bytes.byteOffset + offset, 4);
return view.getUint32(0, false);
};
static #parseHeaderBlock = (bytes = new Uint8Array([])) => {
const text = TEXT_DECODER.decode(bytes);
const header = {};
for (const line of text.split("\r\n")) {
if (!line) continue;
const separatorIndex = line.indexOf(":");
if (separatorIndex <= 0) continue;
const key = line.slice(0, separatorIndex).trim().toLowerCase();
const value = line.slice(separatorIndex + 1).trim();
if (!key) continue;
header[key] = key in header ? `${header[key]}, ${value}` : value;
}
return header;
};
// 计算校验和 (B站为数据本体字节数)
static #Checksum = (num = 0) => {
const array = new ArrayBuffer(4); // an Int32 takes 4 bytes
const view = new DataView(array);
// 首位填充计算过的新数据长度
view.setUint32(0, num, false); // byteOffset = 0; litteEndian = false
return new Uint8Array(array);
};
}