diff --git a/packages/linejs/base/login/mod.ts b/packages/linejs/base/login/mod.ts index d090f22..556aea2 100644 --- a/packages/linejs/base/login/mod.ts +++ b/packages/linejs/base/login/mod.ts @@ -180,24 +180,45 @@ export class Login { public async requestSQR(): Promise { const { 1: sqr } = await this.createSession(); - let { 1: url } = await this.createQrCode(sqr); + let { + 1: url, + 2: longPollingMaxCount, + 3: longPollingIntervalSec, + 4: nonce, + } = await this.createQrCodeForSecure(sqr); const [secret, secretUrl] = this.client.e2ee.createSqrSecret(); url = url + secretUrl; this.client.emit("qrcall", url); - if (await this.checkQrCodeVerified(sqr)) { + if ( + await this.checkQrCodeVerified( + sqr, + longPollingMaxCount, + longPollingIntervalSec, + ) + ) { try { await this.verifyCertificate(sqr, await this.getQrCert()); } catch (_e) { const { 1: pincode } = await this.createPinCode(sqr); this.client.emit("pincall", pincode); - await this.checkPinCodeVerified(sqr); + await this.checkPinCodeVerified( + sqr, + longPollingMaxCount, + longPollingIntervalSec, + ); + } + const response = await this.qrCodeLoginForSecure(sqr, nonce); + const { 1: pem, 2: authToken, 4: metaData } = response; + const mid = response[5]; // Legacy? or check metadata + if (mid && !this.client.profile) { + this.client.profile = { mid } as any; } - const response = await this.qrCodeLogin(sqr); - const { 1: pem, 2: authToken, 4: e2eeInfo, 5: _mid } = response; if (pem) { this.client.emit("update:qrcert", pem); await this.registerQrCert(pem); } + // MetaData contains E2EE info in some versions + const e2eeInfo = metaData?.["e2eeInfo"]; if (e2eeInfo) { await this.client.e2ee.decodeE2EEKeyV1( e2eeInfo, @@ -214,24 +235,47 @@ export class Login { public async requestSQR2(): Promise { const { 1: sqr } = await this.createSession(); - let { 1: url } = await this.createQrCode(sqr); + let { + 1: url, + 2: longPollingMaxCount, + 3: longPollingIntervalSec, + 4: nonce, + } = await this.createQrCodeForSecure(sqr); const [secret, secretUrl] = this.client.e2ee.createSqrSecret(); url = url + secretUrl; this.client.emit("qrcall", url); - if (await this.checkQrCodeVerified(sqr)) { + if ( + await this.checkQrCodeVerified( + sqr, + longPollingMaxCount, + longPollingIntervalSec, + ) + ) { try { await this.verifyCertificate(sqr, await this.getQrCert()); } catch (_e) { const { 1: pincode } = await this.createPinCode(sqr); this.client.emit("pincall", pincode); - await this.checkPinCodeVerified(sqr); + await this.checkPinCodeVerified( + sqr, + longPollingMaxCount, + longPollingIntervalSec, + ); + } + const response = await this.qrCodeLoginV2ForSecure(sqr, nonce); + const { 1: pem, 3: tokenInfo, 4: mid, 10: _metaData } = response; + + if (mid) { + this.client.profile = { mid } as any; } - const response = await this.qrCodeLoginV2(sqr); - const { 1: pem, 3: tokenInfo, 4: _mid, 10: e2eeInfo } = response; + if (pem) { this.client.emit("update:qrcert", pem); await this.registerQrCert(pem); } + + // In V2, E2EE info might be elsewhere or in metaData + const e2eeInfo = _metaData?.[11]; // Just in case field 11 is e2ee if (e2eeInfo) { await this.client.e2ee.decodeE2EEKeyV1( e2eeInfo, @@ -243,7 +287,6 @@ export class Login { "expire", tokenInfo[3] + tokenInfo[6], ); - console.log(tokenInfo); return tokenInfo[1]; } throw new InternalError( @@ -640,24 +683,37 @@ export class Login { ); } - public async checkQrCodeVerified(qrcode: string): Promise { - try { - await this.client.request.request( - [[12, 1, [[11, 1, qrcode]]]], - "checkQrCodeVerified", - 4, - false, - "/acct/lp/lgn/sq/v1", - { - "x-lst": "180000", - "x-line-access": qrcode, - }, - this.client.config.longTimeout, - ); - return true; - } catch (error) { - throw error; + public async checkQrCodeVerified( + qrcode: string, + maxCount: number = 1, + interval: number = 30, + ): Promise { + for (let i = 0; i < maxCount; i++) { + try { + await this.client.request.request( + [[12, 1, [[11, 1, qrcode]]]], + "checkQrCodeVerified", + 4, + false, + "/acct/lp/lgn/sq/v1", + { + "x-lst": (interval * 1000).toString(), + "x-line-access": qrcode, + }, + interval * 1000 + 5000, + ); + return true; + } catch (error) { + if ( + error instanceof InternalError && + error.message.includes("Timeout") + ) { + continue; + } + throw error; + } } + return false; } public async verifyCertificate( @@ -683,24 +739,37 @@ export class Login { ); } - public async checkPinCodeVerified(qrcode: string): Promise { - try { - await this.client.request.request( - [[12, 1, [[11, 1, qrcode]]]], - "checkPinCodeVerified", - 4, - false, - "/acct/lp/lgn/sq/v1", - { - "x-lst": "180000", - "x-line-access": qrcode, - }, - this.client.config.longTimeout, - ); - return true; - } catch (error) { - throw error; + public async checkPinCodeVerified( + qrcode: string, + maxCount: number = 1, + interval: number = 30, + ): Promise { + for (let i = 0; i < maxCount; i++) { + try { + await this.client.request.request( + [[12, 1, [[11, 1, qrcode]]]], + "checkPinCodeVerified", + 4, + false, + "/acct/lp/lgn/sq/v1", + { + "x-lst": (interval * 1000).toString(), + "x-line-access": qrcode, + }, + interval * 1000 + 5000, + ); + return true; + } catch (error) { + if ( + error instanceof InternalError && + error.message.includes("Timeout") + ) { + continue; + } + throw error; + } } + return false; } public async qrCodeLogin( @@ -740,6 +809,58 @@ export class Login { ); } + public async createQrCodeForSecure(authSessionId: string): Promise { + return await this.client.request.request( + [[12, 1, [[11, 1, authSessionId]]]], + "createQrCodeForSecure", + 4, + false, + "/acct/lgn/sq/v1", + ); + } + + public async qrCodeLoginV2ForSecure( + authSessionId: string, + nonce: string, + systemName: string = "linejs-v2", + modelName: string = "evex-device", + autoLoginIsRequired: boolean = true, + ): Promise { + return await this.client.request.request( + [[12, 1, [ + [11, 1, authSessionId], + [11, 2, systemName], + [11, 3, modelName], + [2, 4, autoLoginIsRequired], + [11, 5, nonce], + ]]], + "qrCodeLoginV2ForSecure", + 4, + false, + "/acct/lgn/sq/v1", + ); + } + + public async qrCodeLoginForSecure( + authSessionId: string, + nonce: string, + systemName: string = "linejs-v2", + autoLoginIsRequired: boolean = true, + ): Promise { + return await this.client.request.request( + [[12, 1, [ + [11, 1, authSessionId], + [11, 2, systemName], + [2, 3, autoLoginIsRequired], + [11, 4, nonce], + ]]], + "qrCodeLoginForSecure", + 4, + false, + "/acct/lgn/sq/v1", + ); + } + public async confirmE2EELogin( verifier: string, deviceSecret: Buffer, diff --git a/packages/linejs/base/push/conn.ts b/packages/linejs/base/push/conn.ts index 7797b00..edc4eaa 100644 --- a/packages/linejs/base/push/conn.ts +++ b/packages/linejs/base/push/conn.ts @@ -1,3 +1,4 @@ +import { PushProtocol } from "./protocol.ts"; import { BaseClient } from "../core/mod.ts"; import { LegyH2PingFrame, @@ -16,7 +17,7 @@ export class Conn { cacheData: Uint8Array = new Uint8Array(0); notFinPayloads: Record = {}; reqStream?: ReadableStreamWriter & { abort: AbortController }; - resStream?: ReadableStream; + resStream?: ReadableStream>; private _lastSendTime = 0; private _closed = false; @@ -39,7 +40,7 @@ export class Conn { const chunks: Uint8Array[] = []; const encoder = new TextEncoder(); - const stream = new ReadableStream({ + const stream = new ReadableStream>({ start(c) { controller = c; }, @@ -129,7 +130,7 @@ export class Conn { } async writeRequest(requestType: number, data: Uint8Array) { - const d = this.manager.buildRequest(requestType, data); + const d = PushProtocol.buildRequest(requestType, data); await this.writeByte(d); } diff --git a/packages/linejs/base/push/connManager.ts b/packages/linejs/base/push/connManager.ts index 82ebc64..910b394 100644 --- a/packages/linejs/base/push/connManager.ts +++ b/packages/linejs/base/push/connManager.ts @@ -1,3 +1,4 @@ +import { PushProtocol } from "./protocol.ts"; import { LegyH2PushFrame } from "./connData.ts"; import { Conn } from "./conn.ts"; import { BaseClient } from "@evex/linejs/base"; @@ -6,7 +7,6 @@ import { TCompactProtocol } from "npm:thrift@^0.20.0"; import { TMoreCompactProtocol } from "../thrift/readwrite/tmc.ts"; -// GOMI: import { PartialDeep, SquareService_fetchMyEvents_args as gen_SquareService_fetchMyEvents_args, @@ -25,11 +25,7 @@ import { import { ParsedThrift } from "@evex/linejs/thrift"; import { Buffer } from "node:buffer"; -function gen_m(ss = [1, 3, 5, 6, 8, 9, 10]) { - let i = 0; - for (const s of ss) i |= 1 << (s - 1); - return i; -} + export interface ReadableStreamWriter { stream: ReadableStream; @@ -46,7 +42,7 @@ export class ConnManager { SignOnRequests: Record = {}; OnPingCallback: (id: number) => void; OnSignReqResp: Record = {}; - OnSignOnResponse: (reqId: number, isFin: boolean, data: Uint8Array) => void; + OnSignOnResponse: (reqId: number, isFin: boolean, data: Uint8Array) => void; OnPushResponse: (frame: LegyH2PushFrame) => void; _eventSynced = false; _pingInterval = 30; @@ -150,7 +146,7 @@ export class ConnManager { .getHeader(); tosendHeaders["content-type"] = "application/octet-stream"; tosendHeaders["accept"] = "application/octet-stream"; - const m = gen_m(initServices); + const m = PushProtocol.genServiceBitmask(initServices); this.log(`Using \`m=${m}\` on \`/PUSH\``); const host = this.client.request.endpoint; const port = 443; @@ -158,30 +154,19 @@ export class ConnManager { return _conn; } - buildRequest(service: number, data: Uint8Array): Uint8Array { - const len = data.length; - const out = new Uint8Array(2 + 1 + len); - out[0] = (len >> 8) & 0xff; - out[1] = len & 0xff; - out[2] = service & 0xff; - out.set(data, 3); - return out; - } + async buildAndSendSignOnRequest( conn: Conn, serviceType: number, kwargs: Record = {}, ): Promise<{ - payload: Uint8Array; + payload: Uint8Array; id: number; }> { this.log("buildAndSendSignOnRequest", { serviceType, kwargs }); const cl = this.client; const id = Object.keys(this.SignOnRequests).length + 1; - const idBuf = new Uint8Array(2); - idBuf[0] = (id >> 8) & 0xff; - idBuf[1] = id & 0xff; let methodName: string | undefined; // build payload body depending on serviceType let req = new Uint8Array(0); @@ -203,13 +188,7 @@ export class ConnManager { ); methodName = "sync"; } - const header = new Uint8Array(2 + 1 + 1 + 2 + req.length); - header.set(idBuf, 0); - header[2] = serviceType & 0xff; - header[3] = 0; - header[4] = (req.length >> 8) & 0xff; - header[5] = req.length & 0xff; - header.set(req, 6); + const header = PushProtocol.buildSignOnRequest(id, serviceType, req); this.SignOnRequests[id] = [serviceType, methodName, null]; this.log( `[H2][PUSH] send sign-on-request. requestId:${id}, service:${serviceType}`, @@ -221,7 +200,7 @@ export class ConnManager { async _OnSignOnResponse( reqId: number, isFin: boolean, - data: Uint8Array, + data: Uint8Array, ): Promise { // data = data.slice(5); const cl = this.client; diff --git a/packages/linejs/base/push/protocol.ts b/packages/linejs/base/push/protocol.ts new file mode 100644 index 0000000..7897d16 --- /dev/null +++ b/packages/linejs/base/push/protocol.ts @@ -0,0 +1,50 @@ +export class PushProtocol { + /** + * Generates a service bitmask for the PUSH connection. + */ + static genServiceBitmask(ss: number[] = [1, 3, 5, 6, 8, 9, 10]): number { + let i = 0; + for (const s of ss) i |= 1 << (s - 1); + return i; + } + + /** + * Builds a standard push request frame. + * [2 bytes length][1 byte service type][payload] + */ + static buildRequest(service: number, data: Uint8Array): Uint8Array { + const len = data.length; + const out = new Uint8Array(3 + len); + out[0] = (len >> 8) & 0xff; + out[1] = len & 0xff; + out[2] = service & 0xff; + out.set(data, 3); + return out; + } + + /** + * Builds a SignOn request packet. + * [2 bytes request id][1 byte service type][1 byte unknown(0)][2 bytes payload length][payload] + */ + static buildSignOnRequest( + requestId: number, + serviceType: number, + payload: Uint8Array, + ): Uint8Array { + const len = payload.length; + const out = new Uint8Array(6 + len); + // Request ID (2 bytes) + out[0] = (requestId >> 8) & 0xff; + out[1] = requestId & 0xff; + // Service Type (1 byte) + out[2] = serviceType & 0xff; + // Unknown (1 byte, always 0) + out[3] = 0; + // Payload Length (2 bytes) + out[4] = (len >> 8) & 0xff; + out[5] = len & 0xff; + // Payload + out.set(payload, 6); + return out; + } +}