Skip to content

Commit e4e4454

Browse files
authored
Add legyfetch.js customFetch (#121)
* Add legyfetch.js for LINE API integration * Remove console logs from resolveToken function Removed console log statements for token resolution.
1 parent afa397b commit e4e4454

1 file changed

Lines changed: 216 additions & 0 deletions

File tree

example/legy/legyfetch.js

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { BaseClient } from "@evex/linejs/base";
2+
import { LINEStruct } from "@evex/linejs/thrift";
3+
import crypto from "node:crypto";
4+
import https from "node:https";
5+
import xxhashInit from "xxhash-wasm";
6+
7+
const APP_VER = "26.2.0";
8+
const SYSTEM_NAME = "Android OS";
9+
const SYSTEM_VER = "15";
10+
const X_LINE_APP = `ANDROID\t${APP_VER}\t${SYSTEM_NAME}\t${SYSTEM_VER}`;
11+
const USER_AGENT = `Line/${APP_VER}`;
12+
13+
// authKeyでもPrimaryTokenでも大丈夫
14+
const AUTH_KEY = "u***************:++++++++++++++++++";
15+
16+
function createToken(authKey) {
17+
const [mid, ...rest] = authKey.split(":");
18+
const key = Buffer.from(rest.join(":"), "base64");
19+
const iat =
20+
Buffer.from(`iat: ${Math.floor(Date.now() / 1000) * 60}\n`, "utf-8").toString("base64") + ".";
21+
const digest = crypto.createHmac("sha1", key).update(iat).digest("base64");
22+
return `${mid}:${iat}.${digest}`;
23+
}
24+
25+
function isAlreadyToken(value) {
26+
const idx = value.indexOf(":");
27+
if (idx === -1) return false;
28+
const payload = value.substring(idx + 1);
29+
try {
30+
const decoded = Buffer.from(payload.split(".")[0], "base64").toString("utf-8");
31+
return decoded.startsWith("iat:");
32+
} catch {
33+
return false;
34+
}
35+
}
36+
37+
function resolveToken(value) {
38+
if (isAlreadyToken(value)) {
39+
return value;
40+
}
41+
return createToken(value);
42+
}
43+
44+
// legy enc
45+
const LINE_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
46+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsMC6HAYeMq4R59e2yRw6
47+
W1OWT2t9aepiAp4fbSCXzRj7A29BOAFAvKlzAub4oxN13Nt8dbcB+ICAufyDnN5N
48+
d3+vXgDxEXZ/sx2/wuFbC3B3evSNKR4hKcs80suRs8aL6EeWi+bAU2oYIc78Bbqh
49+
Nzx0WCzZSJbMBFw1VlsU/HQ/XdiUufopl5QSa0S246XXmwJmmXRO0v7bNvrxaNV0
50+
cbviGkOvTlBt1+RerIFHMTw3SwLDnCOolTz3CuE5V2OrPZCmC0nlmPRzwUfxoxxs
51+
/6qFdpZNoORH/s5mQenSyqPkmH8TBOlHJWPH3eN1k6aZIlK5S54mcUb/oNRRq9wD
52+
1wIDAQAB
53+
-----END PUBLIC KEY-----`;
54+
55+
const LEGY_IV = Buffer.from([78, 9, 72, 62, 56, 245, 255, 114, 128, 18, 123, 158, 251, 92, 45, 51]);
56+
const LEGY_LE = "7";
57+
const LEGY_LCS_PREFIX = "0008";
58+
const LEGY_GF_URL = "https://gf.line.naver.jp/enc";
59+
const leGyAesKey = crypto.randomBytes(16);
60+
const xLcs = LEGY_LCS_PREFIX + crypto.publicEncrypt(
61+
{ key: LINE_PUBLIC_KEY, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha1" },
62+
leGyAesKey,
63+
).toString("base64");
64+
let xxh = null;
65+
66+
// Serialize
67+
function encHeaders(headers) {
68+
const keys = Object.keys(headers);
69+
const parts = [];
70+
parts.push(Buffer.from([(keys.length >> 8) & 0xff, keys.length & 0xff]));
71+
for (const k of keys) {
72+
const kBuf = Buffer.from(k, "ascii");
73+
const vBuf = Buffer.from(headers[k], "ascii");
74+
parts.push(Buffer.from([(kBuf.length >> 8) & 0xff, kBuf.length & 0xff]));
75+
parts.push(kBuf);
76+
parts.push(Buffer.from([(vBuf.length >> 8) & 0xff, vBuf.length & 0xff]));
77+
parts.push(vBuf);
78+
}
79+
const body = Buffer.concat(parts);
80+
return Buffer.concat([Buffer.from([(body.length >> 8) & 0xff, body.length & 0xff]), body]);
81+
}
82+
83+
// parse
84+
function decHeaders(data) {
85+
let off = 0;
86+
const ri16 = () => { const v = (data[off] << 8) | data[off + 1]; off += 2; return v; };
87+
const dataLen = ri16() + 2;
88+
const count = ri16();
89+
const headers = {};
90+
for (let i = 0; i < count; i++) {
91+
const kl = ri16(); const k = data.subarray(off, off + kl).toString("ascii"); off += kl;
92+
const vl = ri16(); const v = data.subarray(off, off + vl).toString("ascii"); off += vl;
93+
headers[k] = v;
94+
}
95+
return { headers, data: data.subarray(dataLen) };
96+
}
97+
98+
function pkcs7Unpad(buf) {
99+
const n = buf[buf.length - 1];
100+
return (n > 0 && n <= 16) ? buf.subarray(0, buf.length - n) : buf;
101+
}
102+
103+
function pkcs7Pad(buf, bs) {
104+
const n = bs - (buf.length % bs);
105+
return Buffer.concat([buf, Buffer.alloc(n, n)]);
106+
}
107+
108+
// AES-128-CBC enc
109+
function leGyEncrypt(pt) {
110+
const c = crypto.createCipheriv("aes-128-cbc", leGyAesKey, LEGY_IV);
111+
c.setAutoPadding(true);
112+
return Buffer.concat([c.update(pt), c.final()]);
113+
}
114+
115+
// AES-128-CBC dec
116+
function leGyDecrypt(ct) {
117+
const padded = pkcs7Pad(ct, 16);
118+
const d = crypto.createDecipheriv("aes-128-cbc", leGyAesKey, LEGY_IV);
119+
d.setAutoPadding(false);
120+
const dec = Buffer.concat([d.update(padded), d.final()]);
121+
return pkcs7Unpad(dec.subarray(0, dec.length - 16));
122+
}
123+
124+
// xxHash32 HMAC
125+
function leGyHmac(key, data, h) {
126+
const opad = Buffer.alloc(16);
127+
const ipad = Buffer.alloc(16);
128+
for (let i = 0; i < 16; i++) {
129+
opad[i] = 0x5c ^ key[i];
130+
ipad[i] = 0x36 ^ key[i];
131+
}
132+
const innerHex = (h.h32Raw(Buffer.concat([ipad, data]), 0) >>> 0).toString(16).padStart(8, "0");
133+
const outerHex = (h.h32Raw(Buffer.concat([opad, Buffer.from(innerHex, "hex")]), 0) >>> 0).toString(16).padStart(8, "0");
134+
return Buffer.from(outerHex, "hex");
135+
}
136+
137+
async function leGyFetch(request) {
138+
if (!xxh) xxh = await xxhashInit();
139+
const url = new URL(request.url);
140+
const path = url.pathname;
141+
const thriftBody = Buffer.from(await request.arrayBuffer());
142+
const token = request.headers.get("x-line-access");
143+
// legy header
144+
const inner = token
145+
? { "x-lt": token, "x-lpqs": path }
146+
: { "x-lpqs": path };
147+
const plaintext = Buffer.concat([encHeaders(inner), thriftBody]);
148+
const leInt = parseInt(LEGY_LE, 10);
149+
const fixBytes = (leInt & 4) === 4;
150+
let toEncrypt = fixBytes
151+
? Buffer.concat([Buffer.from([leInt]), plaintext])
152+
: plaintext;
153+
let enc = leGyEncrypt(toEncrypt);
154+
if ((leInt & 2) === 2) {
155+
enc = Buffer.concat([enc, leGyHmac(leGyAesKey, enc, xxh)]);
156+
}
157+
// gf.line.naver.jp/enc
158+
const { statusCode, responseBody, responseHeaders } = await new Promise((resolve, reject) => {
159+
const gfUrl = new URL(LEGY_GF_URL);
160+
const req = https.request({
161+
hostname: gfUrl.hostname,
162+
port: 443,
163+
path: gfUrl.pathname,
164+
method: "POST",
165+
headers: {
166+
"x-line-application": X_LINE_APP,
167+
"x-le": LEGY_LE,
168+
"x-lap": "5",
169+
"x-lpv": "1",
170+
"x-lcs": xLcs,
171+
"User-Agent": USER_AGENT,
172+
"content-type": "application/x-thrift; protocol=TBINARY",
173+
"x-lal": "ja_JP",
174+
"x-lhm": "POST",
175+
"accept": "*/*",
176+
"accept-encoding": "gzip, deflate",
177+
"connection": "keep-alive",
178+
"Content-Length": enc.length,
179+
},
180+
}, (res) => {
181+
const chunks = [];
182+
res.on("data", (chunk) => chunks.push(chunk));
183+
res.on("end", () => resolve({
184+
statusCode: res.statusCode,
185+
responseBody: Buffer.concat(chunks),
186+
responseHeaders: res.headers,
187+
}));
188+
});
189+
req.on("error", reject);
190+
req.write(enc);
191+
req.end();
192+
});
193+
if (!responseBody.length) {
194+
return new Response(responseBody, { status: statusCode });
195+
}
196+
// legy dec
197+
let dec = leGyDecrypt(responseBody);
198+
if (fixBytes) dec = dec.subarray(1);
199+
const { headers: innerH, data: thriftData } = decHeaders(dec);
200+
const innerStatus = innerH["x-lc"];
201+
const httpStatus = (innerStatus && innerStatus !== "200") ? parseInt(innerStatus, 10) : statusCode;
202+
return new Response(thriftData, {
203+
status: httpStatus,
204+
headers: { "content-type": "application/x-thrift" },
205+
});
206+
}
207+
208+
// linejs BaseClient
209+
const base = new BaseClient({ device: "ANDROID", version: APP_VER, fetch: leGyFetch });
210+
base.authToken = resolveToken(AUTH_KEY);
211+
base.request.systemType = X_LINE_APP;
212+
base.request.userAgent = USER_AGENT;
213+
base.talk.requestPath = "/S3";
214+
base.talk.protocolType = 3;
215+
216+
export { base, LINEStruct };

0 commit comments

Comments
 (0)