eufy-security-client-fork
Version:
Client to comunicate with Eufy-Security devices
288 lines • 11.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PushClient = void 0;
const Long = require("long");
const path = require("path");
const protobuf_typescript_1 = require("protobuf-typescript");
const tls = require("tls");
const tiny_typed_emitter_1 = require("tiny-typed-emitter");
const ts_log_1 = require("ts-log");
const models_1 = require("./models");
const parser_1 = require("./parser");
class PushClient extends tiny_typed_emitter_1.TypedEmitter {
constructor(pushClientParser, auth, log = ts_log_1.dummyLogger) {
super();
this.HOST = "mtalk.google.com";
this.PORT = 5228;
this.MCS_VERSION = 41;
this.HEARTBEAT_INTERVAL = 5 * 60 * 1000;
this.loggedIn = false;
this.streamId = 0;
this.lastStreamIdReported = -1;
this.currentDelay = 0;
this.persistentIds = [];
this.log = log;
this.pushClientParser = pushClientParser;
this.auth = auth;
}
static async init(auth, log = ts_log_1.dummyLogger) {
this.proto = await (0, protobuf_typescript_1.load)(path.join(__dirname, "./proto/mcs.proto"));
const pushClientParser = await parser_1.PushClientParser.init(log);
return new PushClient(pushClientParser, auth, log);
}
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.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(errorMessage);
}
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;
}
this.log.debug(`heartbeatPingRequest`, heartbeatPingRequest);
const HeartbeatPingRequestType = PushClient.proto.lookupType("mcs_proto.HeartbeatPing");
const errorMessage = HeartbeatPingRequestType.verify(heartbeatPingRequest);
if (errorMessage) {
throw new Error(errorMessage);
}
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;
}
this.log.debug(`heartbeatAckRequest`, heartbeatAckRequest);
const HeartbeatAckRequestType = PushClient.proto.lookupType("mcs_proto.HeartbeatAck");
const errorMessage = HeartbeatAckRequestType.verify(heartbeatAckRequest);
if (errorMessage) {
throw new Error(errorMessage);
}
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(error) {
this.log.error(`onSocketError:`, error);
}
handleParsedMessage(message) {
this.resetCurrentDelay();
switch (message.tag) {
case models_1.MessageTag.DataMessageStanza:
this.log.debug(`DataMessageStanza`, 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:
this.log.debug(`Close: Server requested close`, message);
break;
case models_1.MessageTag.LoginResponse:
this.log.debug("Login response: GCM -> logged in -> waiting for push messages...");
this.loggedIn = true;
this.persistentIds = [];
this.emit("connect");
this.heartbeatTimeout = setTimeout(() => {
this.scheduleHeartbeat(this);
}, this.getHeartbeatInterval());
break;
case models_1.MessageTag.LoginRequest:
this.log.debug(`Login request`, message);
break;
case models_1.MessageTag.IqStanza:
this.log.debug(`IqStanza: Not implemented`, message);
break;
default:
this.log.debug(`Unknown message`, message);
return;
}
this.streamId++;
}
handleHeartbeatPing(message) {
this.log.debug(`Heartbeat ping`, 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) {
this.log.debug(`Heartbeat acknowledge`, message);
}
convertPayloadMessage(message) {
const { appData, ...otherData } = message.object;
const messageData = {};
appData.forEach((kv) => {
if (kv.key === "payload") {
const payload = JSON.parse(Buffer.from(kv.value, "base64").toString("utf8"));
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 {
this.log.debug("Heartbeat disabled!");
}
}
sendHeartbeat() {
let streamId = undefined;
if (this.newStreamIdAvailable()) {
streamId = this.getStreamId();
}
if (this.client && this.isConnected()) {
this.log.debug(`Sending heartbeat...`, streamId);
this.client.write(this.buildHeartbeatPingRequest(streamId));
return true;
}
else {
this.log.debug("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();
this.log.debug("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;
PushClient.proto = null;
//# sourceMappingURL=client.js.map