323 lines
13 KiB
JavaScript
323 lines
13 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.PushClient = void 0;
|
|
const long_1 = __importDefault(require("long"));
|
|
const path = __importStar(require("path"));
|
|
const protobufjs_1 = require("protobufjs");
|
|
const tls = __importStar(require("tls"));
|
|
const tiny_typed_emitter_1 = require("tiny-typed-emitter");
|
|
const models_1 = require("./models");
|
|
const parser_1 = require("./parser");
|
|
const utils_1 = require("../utils");
|
|
const error_1 = require("./error");
|
|
const error_2 = require("../error");
|
|
const utils_2 = require("../p2p/utils");
|
|
const logging_1 = require("../logging");
|
|
class PushClient extends tiny_typed_emitter_1.TypedEmitter {
|
|
HOST = "mtalk.google.com";
|
|
PORT = 5228;
|
|
MCS_VERSION = 41;
|
|
HEARTBEAT_INTERVAL = 5 * 60 * 1000;
|
|
loggedIn = false;
|
|
streamId = 0;
|
|
lastStreamIdReported = -1;
|
|
currentDelay = 0;
|
|
client;
|
|
heartbeatTimeout;
|
|
reconnectTimeout;
|
|
persistentIds = [];
|
|
static proto = null;
|
|
pushClientParser;
|
|
auth;
|
|
constructor(pushClientParser, auth) {
|
|
super();
|
|
this.pushClientParser = pushClientParser;
|
|
this.auth = auth;
|
|
}
|
|
static async init(auth) {
|
|
this.proto = await (0, protobufjs_1.load)(path.join(__dirname, "./proto/mcs.proto"));
|
|
const pushClientParser = await parser_1.PushClientParser.init();
|
|
return new PushClient(pushClientParser, auth);
|
|
}
|
|
initialize() {
|
|
this.loggedIn = false;
|
|
this.streamId = 0;
|
|
this.lastStreamIdReported = -1;
|
|
if (this.client) {
|
|
this.client.removeAllListeners();
|
|
this.client.destroy();
|
|
this.client = undefined;
|
|
}
|
|
this.pushClientParser.resetState();
|
|
if (this.reconnectTimeout) {
|
|
clearTimeout(this.reconnectTimeout);
|
|
this.reconnectTimeout = undefined;
|
|
}
|
|
}
|
|
getPersistentIds() {
|
|
return this.persistentIds;
|
|
}
|
|
setPersistentIds(ids) {
|
|
this.persistentIds = ids;
|
|
}
|
|
connect() {
|
|
this.initialize();
|
|
this.pushClientParser.on("message", (message) => this.handleParsedMessage(message));
|
|
this.client = tls.connect(this.PORT, this.HOST, {
|
|
rejectUnauthorized: false,
|
|
});
|
|
this.client.setKeepAlive(true);
|
|
// For debugging purposes
|
|
//this.client.enableTrace();
|
|
this.client.on("connect", () => this.onSocketConnect());
|
|
this.client.on("close", () => this.onSocketClose());
|
|
this.client.on("error", (error) => this.onSocketError(error));
|
|
this.client.on("data", (newData) => this.onSocketData(newData));
|
|
this.client.write(this.buildLoginRequest());
|
|
}
|
|
buildLoginRequest() {
|
|
const androidId = this.auth.androidId;
|
|
const securityToken = this.auth.securityToken;
|
|
const LoginRequestType = PushClient.proto.lookupType("mcs_proto.LoginRequest");
|
|
const hexAndroidId = long_1.default.fromString(androidId).toString(16);
|
|
const loginRequest = {
|
|
adaptiveHeartbeat: false,
|
|
authService: 2,
|
|
authToken: securityToken,
|
|
id: "chrome-63.0.3234.0",
|
|
domain: "mcs.android.com",
|
|
deviceId: `android-${hexAndroidId}`,
|
|
networkType: 1,
|
|
resource: androidId,
|
|
user: androidId,
|
|
useRmq2: true,
|
|
setting: [{ name: "new_vc", value: "1" }],
|
|
clientEvent: [],
|
|
receivedPersistentId: this.persistentIds,
|
|
};
|
|
const errorMessage = LoginRequestType.verify(loginRequest);
|
|
if (errorMessage) {
|
|
throw new error_1.BuildLoginRequestError(errorMessage, { context: { loginRequest: loginRequest } });
|
|
}
|
|
const buffer = LoginRequestType.encodeDelimited(loginRequest).finish();
|
|
return Buffer.concat([Buffer.from([this.MCS_VERSION, models_1.MessageTag.LoginRequest]), buffer]);
|
|
}
|
|
buildHeartbeatPingRequest(stream_id) {
|
|
const heartbeatPingRequest = {};
|
|
if (stream_id) {
|
|
heartbeatPingRequest.last_stream_id_received = stream_id;
|
|
}
|
|
logging_1.rootPushLogger.debug(`Push client - heartbeatPingRequest`, { streamId: stream_id, request: JSON.stringify(heartbeatPingRequest) });
|
|
const HeartbeatPingRequestType = PushClient.proto.lookupType("mcs_proto.HeartbeatPing");
|
|
const errorMessage = HeartbeatPingRequestType.verify(heartbeatPingRequest);
|
|
if (errorMessage) {
|
|
throw new error_1.BuildHeartbeatPingRequestError(errorMessage, { context: { heartbeatPingRequest: heartbeatPingRequest } });
|
|
}
|
|
const buffer = HeartbeatPingRequestType.encodeDelimited(heartbeatPingRequest).finish();
|
|
return Buffer.concat([Buffer.from([models_1.MessageTag.HeartbeatPing]), buffer]);
|
|
}
|
|
buildHeartbeatAckRequest(stream_id, status) {
|
|
const heartbeatAckRequest = {};
|
|
if (stream_id && !status) {
|
|
heartbeatAckRequest.last_stream_id_received = stream_id;
|
|
}
|
|
else if (!stream_id && status) {
|
|
heartbeatAckRequest.status = status;
|
|
}
|
|
else {
|
|
heartbeatAckRequest.last_stream_id_received = stream_id;
|
|
heartbeatAckRequest.status = status;
|
|
}
|
|
logging_1.rootPushLogger.debug(`Push client - heartbeatAckRequest`, { streamId: stream_id, status: status, request: JSON.stringify(heartbeatAckRequest) });
|
|
const HeartbeatAckRequestType = PushClient.proto.lookupType("mcs_proto.HeartbeatAck");
|
|
const errorMessage = HeartbeatAckRequestType.verify(heartbeatAckRequest);
|
|
if (errorMessage) {
|
|
throw new error_1.BuildHeartbeatAckRequestError(errorMessage, { context: { heartbeatAckRequest: heartbeatAckRequest } });
|
|
}
|
|
const buffer = HeartbeatAckRequestType.encodeDelimited(heartbeatAckRequest).finish();
|
|
return Buffer.concat([Buffer.from([models_1.MessageTag.HeartbeatAck]), buffer]);
|
|
}
|
|
onSocketData(newData) {
|
|
this.pushClientParser.handleData(newData);
|
|
}
|
|
onSocketConnect() {
|
|
//
|
|
}
|
|
onSocketClose() {
|
|
this.loggedIn = false;
|
|
if (this.heartbeatTimeout) {
|
|
clearTimeout(this.heartbeatTimeout);
|
|
this.heartbeatTimeout = undefined;
|
|
}
|
|
this.emit("close");
|
|
this.scheduleReconnect();
|
|
}
|
|
onSocketError(err) {
|
|
const error = (0, error_2.ensureError)(err);
|
|
logging_1.rootPushLogger.error(`Push client - Socket Error`, { error: (0, utils_1.getError)(error) });
|
|
}
|
|
handleParsedMessage(message) {
|
|
this.resetCurrentDelay();
|
|
switch (message.tag) {
|
|
case models_1.MessageTag.DataMessageStanza:
|
|
logging_1.rootPushLogger.debug(`Push client - DataMessageStanza`, { message: JSON.stringify(message) });
|
|
if (message.object && message.object.persistentId)
|
|
this.persistentIds.push(message.object.persistentId);
|
|
this.emit("message", this.convertPayloadMessage(message));
|
|
break;
|
|
case models_1.MessageTag.HeartbeatPing:
|
|
this.handleHeartbeatPing(message);
|
|
break;
|
|
case models_1.MessageTag.HeartbeatAck:
|
|
this.handleHeartbeatAck(message);
|
|
break;
|
|
case models_1.MessageTag.Close:
|
|
logging_1.rootPushLogger.debug(`Push client - Close: Server requested close`, { message: JSON.stringify(message) });
|
|
break;
|
|
case models_1.MessageTag.LoginResponse:
|
|
logging_1.rootPushLogger.debug("Push client - Login response: GCM -> logged in -> waiting for push messages...", { message: JSON.stringify(message) });
|
|
this.loggedIn = true;
|
|
this.persistentIds = [];
|
|
this.emit("connect");
|
|
this.heartbeatTimeout = setTimeout(() => {
|
|
this.scheduleHeartbeat(this);
|
|
}, this.getHeartbeatInterval());
|
|
break;
|
|
case models_1.MessageTag.LoginRequest:
|
|
logging_1.rootPushLogger.debug(`Push client - Login request`, { message: JSON.stringify(message) });
|
|
break;
|
|
case models_1.MessageTag.IqStanza:
|
|
logging_1.rootPushLogger.debug(`Push client - IqStanza: Not implemented`, { message: JSON.stringify(message) });
|
|
break;
|
|
default:
|
|
logging_1.rootPushLogger.debug(`Push client - Unknown message`, { message: JSON.stringify(message) });
|
|
return;
|
|
}
|
|
this.streamId++;
|
|
}
|
|
handleHeartbeatPing(message) {
|
|
logging_1.rootPushLogger.debug(`Push client - Heartbeat ping`, { message: JSON.stringify(message) });
|
|
let streamId = undefined;
|
|
let status = undefined;
|
|
if (this.newStreamIdAvailable()) {
|
|
streamId = this.getStreamId();
|
|
}
|
|
if (message.object && message.object.status) {
|
|
status = message.object.status;
|
|
}
|
|
if (this.client)
|
|
this.client.write(this.buildHeartbeatAckRequest(streamId, status));
|
|
}
|
|
handleHeartbeatAck(message) {
|
|
logging_1.rootPushLogger.debug(`Push client - Heartbeat acknowledge`, { message: JSON.stringify(message) });
|
|
}
|
|
convertPayloadMessage(message) {
|
|
const { appData, ...otherData } = message.object;
|
|
const messageData = {};
|
|
appData.forEach((kv) => {
|
|
if (kv.key === "payload") {
|
|
const payload = (0, utils_1.parseJSON)((0, utils_2.getNullTerminatedString)(Buffer.from(kv.value, "base64"), "utf8"), logging_1.rootPushLogger);
|
|
messageData[kv.key] = payload;
|
|
}
|
|
else {
|
|
messageData[kv.key] = kv.value;
|
|
}
|
|
});
|
|
return {
|
|
...otherData,
|
|
payload: messageData,
|
|
};
|
|
}
|
|
getStreamId() {
|
|
this.lastStreamIdReported = this.streamId;
|
|
return this.streamId;
|
|
}
|
|
newStreamIdAvailable() {
|
|
return this.lastStreamIdReported != this.streamId;
|
|
}
|
|
scheduleHeartbeat(client) {
|
|
if (client.sendHeartbeat()) {
|
|
this.heartbeatTimeout = setTimeout(() => {
|
|
this.scheduleHeartbeat(client);
|
|
}, client.getHeartbeatInterval());
|
|
}
|
|
else {
|
|
logging_1.rootPushLogger.debug("Push client - Heartbeat disabled!");
|
|
}
|
|
}
|
|
sendHeartbeat() {
|
|
let streamId = undefined;
|
|
if (this.newStreamIdAvailable()) {
|
|
streamId = this.getStreamId();
|
|
}
|
|
if (this.client && this.isConnected()) {
|
|
logging_1.rootPushLogger.debug(`Push client - Sending heartbeat...`, { streamId: streamId });
|
|
this.client.write(this.buildHeartbeatPingRequest(streamId));
|
|
return true;
|
|
}
|
|
else {
|
|
logging_1.rootPushLogger.debug("Push client - No more connected, reconnect...");
|
|
this.scheduleReconnect();
|
|
}
|
|
return false;
|
|
}
|
|
isConnected() {
|
|
return this.loggedIn;
|
|
}
|
|
getHeartbeatInterval() {
|
|
return this.HEARTBEAT_INTERVAL;
|
|
}
|
|
getCurrentDelay() {
|
|
const delay = this.currentDelay == 0 ? 5000 : this.currentDelay;
|
|
if (this.currentDelay < 60000)
|
|
this.currentDelay += 10000;
|
|
if (this.currentDelay >= 60000 && this.currentDelay < 600000)
|
|
this.currentDelay += 60000;
|
|
return delay;
|
|
}
|
|
resetCurrentDelay() {
|
|
this.currentDelay = 0;
|
|
}
|
|
scheduleReconnect() {
|
|
const delay = this.getCurrentDelay();
|
|
logging_1.rootPushLogger.debug("Push client - Schedule reconnect...", { delay: delay });
|
|
if (!this.reconnectTimeout)
|
|
this.reconnectTimeout = setTimeout(() => {
|
|
this.connect();
|
|
}, delay);
|
|
}
|
|
close() {
|
|
const wasConnected = this.isConnected();
|
|
this.initialize();
|
|
if (wasConnected)
|
|
this.emit("close");
|
|
}
|
|
}
|
|
exports.PushClient = PushClient;
|
|
//# sourceMappingURL=client.js.map
|