eufy-security-client-fork
Version:
Client to comunicate with Eufy-Security devices
878 lines (877 loc) • 117 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.P2PClientProtocol = void 0;
const dgram_1 = require("dgram");
const tiny_typed_emitter_1 = require("tiny-typed-emitter");
const stream_1 = require("stream");
const sweet_collections_1 = require("sweet-collections");
const utils_1 = require("./utils");
const types_1 = require("./types");
const types_2 = require("../http/types");
const device_1 = require("../http/device");
const utils_2 = require("../http/utils");
const talkback_1 = require("./talkback");
const error_1 = require("../error");
const types_3 = require("../push/types");
class P2PClientProtocol extends tiny_typed_emitter_1.TypedEmitter {
constructor(rawStation, api) {
super();
this.MAX_RETRIES = 10;
this.MAX_COMMAND_RESULT_WAIT = 30 * 1000;
this.MAX_AKNOWLEDGE_TIMEOUT = 15 * 1000;
this.MAX_LOOKUP_TIMEOUT = 15 * 1000;
this.LOOKUP_RETRY_TIMEOUT = 150;
this.MAX_EXPECTED_SEQNO_WAIT = 20 * 1000;
this.HEARTBEAT_INTERVAL = 5 * 1000;
this.MAX_COMMAND_QUEUE_TIMEOUT = 120 * 1000;
this.AUDIO_CODEC_ANALYZE_TIMEOUT = 650;
this.KEEPALIVE_INTERVAL = 5 * 1000;
this.ESD_DISCONNECT_TIMEOUT = 15 * 1000;
this.MAX_STREAM_DATA_WAIT = 5 * 1000;
this.UDP_RECVBUFFERSIZE_BYTES = 1048576;
this.MAX_PAYLOAD_BYTES = 1028;
this.MAX_PACKET_BYTES = 1024;
this.MAX_VIDEO_PACKET_BYTES = 655360;
this.P2P_DATA_HEADER_BYTES = 16;
this.MAX_SEQUENCE_NUMBER = 65535;
this.binded = false;
this.connected = false;
this.connecting = false;
this.terminating = false;
this.seqNumber = 0;
this.videoSeqNumber = 0;
this.lockSeqNumber = -1;
this.expectedSeqNo = {};
this.currentMessageBuilder = {};
this.currentMessageState = {};
this.downloadTotalBytes = 0;
this.downloadReceivedBytes = 0;
this.messageStates = new sweet_collections_1.SortedMap((a, b) => a - b);
this.messageVideoStates = new sweet_collections_1.SortedMap((a, b) => a - b);
this.sendQueue = new Array();
this.connectTime = null;
this.lastPong = null;
this.connectionType = types_1.P2PConnectionType.QUICKEST;
this.energySavingDevice = false;
this.energySavingDeviceP2PSeqMapping = new Map();
this.energySavingDeviceP2PDataSeqNumber = 0;
this.connectAddress = undefined;
this.localIPAddress = undefined;
this.dskKey = "";
this.dskExpiration = null;
this.deviceSNs = {};
this.api = api;
this.log = api.getLog();
this.cloudAddresses = (0, utils_1.decodeP2PCloudIPs)(rawStation.app_conn);
this.log.debug("Loaded P2P cloud ip addresses", this.cloudAddresses);
this.updateRawStation(rawStation);
this.socket = (0, dgram_1.createSocket)("udp4");
this.socket.on("message", (msg, rinfo) => this.handleMsg(msg, rinfo));
this.socket.on("error", (error) => this.onError(error));
this.socket.on("close", () => this.onClose());
this._initialize();
}
_incrementSequence(sequence) {
if (sequence < this.MAX_SEQUENCE_NUMBER)
return sequence + 1;
return 0;
}
_initialize() {
let rsaKey;
this.connected = false;
this.connecting = false;
this.lastPong = null;
this.connectTime = null;
this.seqNumber = 0;
this.videoSeqNumber = 0;
this.energySavingDeviceP2PDataSeqNumber = 0;
this.lockSeqNumber = -1;
this.connectAddress = undefined;
this.lastChannel = undefined;
this.lastCustomData = undefined;
this._clearMessageStateTimeouts();
this._clearMessageVideoStateTimeouts();
this.messageStates.clear();
this.messageVideoStates.clear();
this.energySavingDeviceP2PSeqMapping.clear();
for (let datatype = 0; datatype < 4; datatype++) {
this.expectedSeqNo[datatype] = 0;
if (datatype === types_1.P2PDataType.VIDEO)
rsaKey = (0, utils_1.getNewRSAPrivateKey)();
else
rsaKey = null;
this.initializeMessageBuilder(datatype);
this.initializeMessageState(datatype, rsaKey);
this.initializeStream(datatype);
}
}
initializeMessageBuilder(datatype) {
this.currentMessageBuilder[datatype] = {
header: {
commandId: 0,
bytesToRead: 0,
channel: 0,
signCode: 0,
type: 0
},
bytesRead: 0,
messages: {}
};
}
initializeMessageState(datatype, rsaKey = null) {
this.currentMessageState[datatype] = {
leftoverData: Buffer.from([]),
queuedData: new sweet_collections_1.SortedMap((a, b) => a - b),
rsaKey: rsaKey,
videoStream: null,
audioStream: null,
invalidStream: false,
p2pStreaming: false,
p2pStreamNotStarted: true,
p2pStreamChannel: -1,
p2pStreamFirstAudioDataReceived: false,
p2pStreamFirstVideoDataReceived: false,
p2pStreamMetadata: {
videoCodec: types_1.VideoCodec.H264,
videoFPS: 15,
videoHeight: 1080,
videoWidth: 1920,
audioCodec: types_1.AudioCodec.NONE
},
rtspStream: {},
rtspStreaming: {},
receivedFirstIFrame: false,
preFrameVideoData: Buffer.from([]),
p2pTalkback: false,
p2pTalkbackChannel: -1
};
}
_clearTimeout(timeout) {
if (!!timeout) {
clearTimeout(timeout);
}
}
_clearMessageStateTimeouts() {
for (const message of this.messageStates.values()) {
this._clearTimeout(message.timeout);
}
}
_clearMessageVideoStateTimeouts() {
for (const message of this.messageVideoStates.values()) {
this._clearTimeout(message.timeout);
}
}
_clearHeartbeatTimeout() {
this._clearTimeout(this.heartbeatTimeout);
this.heartbeatTimeout = undefined;
}
_clearKeepaliveTimeout() {
this._clearTimeout(this.keepaliveTimeout);
this.keepaliveTimeout = undefined;
}
_clearConnectTimeout() {
this._clearTimeout(this.connectTimeout);
this.connectTimeout = undefined;
}
_clearLookupTimeout() {
this._clearTimeout(this.lookupTimeout);
this.lookupTimeout = undefined;
}
_clearLookupRetryTimeout() {
this._clearTimeout(this.lookupRetryTimeout);
this.lookupRetryTimeout = undefined;
}
_clearESDDisconnectTimeout() {
this._clearTimeout(this.esdDisconnectTimeout);
this.esdDisconnectTimeout = undefined;
}
_clearSecondaryCommandTimeout() {
this._clearTimeout(this.secondaryCommandTimeout);
this.secondaryCommandTimeout = undefined;
}
_disconnected() {
this._clearHeartbeatTimeout();
this._clearKeepaliveTimeout();
this._clearLookupRetryTimeout();
this._clearLookupTimeout();
this._clearConnectTimeout();
this._clearESDDisconnectTimeout();
this._clearSecondaryCommandTimeout();
if (this.currentMessageState[types_1.P2PDataType.VIDEO].p2pStreaming) {
this.endStream(types_1.P2PDataType.VIDEO);
}
if (this.currentMessageState[types_1.P2PDataType.BINARY].p2pStreaming) {
this.endStream(types_1.P2PDataType.BINARY);
}
for (const channel in this.currentMessageState[types_1.P2PDataType.DATA].rtspStreaming) {
this.endRTSPStream(Number.parseInt(channel));
}
this.sendQueue = this.sendQueue.filter((queue) => queue.commandType !== types_1.CommandType.CMD_PING && queue.commandType !== types_1.CommandType.CMD_GET_DEVICE_PING);
if (this.connected) {
this.emit("close");
}
else if (!this.terminating) {
this.emit("timeout");
}
this._initialize();
}
closeEnergySavingDevice() {
if (this.sendQueue.filter((queue) => queue.commandType !== types_1.CommandType.CMD_PING && queue.commandType !== types_1.CommandType.CMD_GET_DEVICE_PING).length === 0 && this.energySavingDevice && !this.isCurrentlyStreaming()) {
if (this.esdDisconnectTimeout === undefined) {
this.log.debug(`Station ${this.rawStation.station_sn} - Energy saving device - No more p2p commands to execute or running streams, initiate disconnect timeout in ${this.ESD_DISCONNECT_TIMEOUT} milliseconds...`);
this.esdDisconnectTimeout = setTimeout(() => {
this.esdDisconnectTimeout = undefined;
(0, utils_1.sendMessage)(this.socket, this.connectAddress, types_1.RequestMessageType.END).catch((error) => {
this.log.error(`Station ${this.rawStation.station_sn} - Error`, error);
});
this.log.info(`Initiated closing of connection to station ${this.rawStation.station_sn} for saving battery.`);
this.terminating = true;
this._disconnected();
}, this.ESD_DISCONNECT_TIMEOUT);
}
}
}
async renewDSKKey() {
if (this.dskKey === "" || (this.dskExpiration && (new Date()).getTime() >= this.dskExpiration.getTime())) {
this.log.debug(`Station ${this.rawStation.station_sn} DSK keys not present or expired, get/renew it`, { dskKey: this.dskKey, dskExpiration: this.dskExpiration });
await this.getDSKKeys();
}
}
localLookup(host) {
this.log.debug(`Trying to local lookup address for station ${this.rawStation.station_sn} with host ${host}`);
this.localLookupByAddress({ host: host, port: 32108 });
}
cloudLookup() {
this.cloudAddresses.map((address) => this.cloudLookupByAddress(address));
this.cloudAddresses.map((address) => this.cloudLookupByAddress2(address));
}
cloudLookup2(origAddress, data) {
this.cloudAddresses.map((address) => this.cloudLookupByAddress3(address, origAddress, data));
}
async localLookupByAddress(address) {
// Send lookup message
const msgId = types_1.RequestMessageType.LOCAL_LOOKUP;
const payload = Buffer.from([0, 0]);
(0, utils_1.sendMessage)(this.socket, address, msgId, payload).catch((error) => {
this.log.error(`Local lookup address for station ${this.rawStation.station_sn} - Error:`, error);
});
}
async cloudLookupByAddress(address) {
// Send lookup message
const msgId = types_1.RequestMessageType.LOOKUP_WITH_KEY;
const payload = (0, utils_1.buildLookupWithKeyPayload)(this.socket, this.rawStation.p2p_did, this.dskKey);
(0, utils_1.sendMessage)(this.socket, address, msgId, payload).catch((error) => {
this.log.error(`Cloud lookup addresses for station ${this.rawStation.station_sn} - Error:`, error);
});
}
async cloudLookupByAddress2(address) {
// Send lookup message2
const msgId = types_1.RequestMessageType.LOOKUP_WITH_KEY2;
const payload = (0, utils_1.buildLookupWithKeyPayload2)(this.rawStation.p2p_did, this.dskKey);
(0, utils_1.sendMessage)(this.socket, address, msgId, payload).catch((error) => {
this.log.error(`Cloud lookup addresses (2) for station ${this.rawStation.station_sn} - Error:`, error);
});
}
cloudLookupByAddress3(address, origAddress, data) {
// Send lookup message3
const msgId = types_1.RequestMessageType.LOOKUP_WITH_KEY3;
const payload = (0, utils_1.buildLookupWithKeyPayload3)(this.rawStation.p2p_did, origAddress, data);
(0, utils_1.sendMessage)(this.socket, address, msgId, payload).catch((error) => {
this.log.error(`Cloud lookup addresses (3) for station ${this.rawStation.station_sn} - Error:`, error);
});
}
isConnected() {
return this.connected;
}
_startConnectTimeout() {
if (this.connectTimeout === undefined)
this.connectTimeout = setTimeout(() => {
this.log.warn(`Station ${this.rawStation.station_sn} - Tried all hosts, no connection could be established`);
this._disconnected();
}, this.MAX_AKNOWLEDGE_TIMEOUT);
}
_connect(address) {
this.log.debug(`Station ${this.rawStation.station_sn} - CHECK_CAM - Connecting to host ${address.host} on port ${address.port}...`);
for (let i = 0; i < 4; i++)
this.sendCamCheck(address);
this._startConnectTimeout();
}
lookup(host) {
if (host === undefined && this.localIPAddress !== undefined) {
host = this.localIPAddress;
}
else {
const localIP = (0, utils_1.getLocalIpAddress)();
host = localIP.substring(0, localIP.lastIndexOf(".") + 1).concat("255");
}
this.localLookup(host);
this.cloudLookup();
this._clearLookupTimeout();
this._clearLookupRetryTimeout();
this.lookupTimeout = setTimeout(() => {
this.lookupTimeout = undefined;
this.log.error(`Station ${this.rawStation.station_sn} - All address lookup tentatives failed.`);
if (this.localIPAddress !== undefined)
this.localIPAddress = undefined;
this._disconnected();
}, this.MAX_LOOKUP_TIMEOUT);
}
async connect(host) {
if (!this.connected && !this.connecting && this.rawStation.p2p_did !== undefined) {
this.connecting = true;
this.terminating = false;
await this.renewDSKKey();
if (!this.binded)
this.socket.bind(0, () => {
this.binded = true;
try {
this.socket.setRecvBufferSize(this.UDP_RECVBUFFERSIZE_BYTES);
}
catch (error) {
this.log.error(`Station ${this.rawStation.station_sn} - Error:`, { error: error, currentRecBufferSize: this.socket.getRecvBufferSize(), recBufferRequestedSize: this.UDP_RECVBUFFERSIZE_BYTES });
}
this.lookup(host);
});
else {
this.lookup(host);
}
}
}
sendCamCheck(address) {
const payload = (0, utils_1.buildCheckCamPayload)(this.rawStation.p2p_did);
(0, utils_1.sendMessage)(this.socket, address, types_1.RequestMessageType.CHECK_CAM, payload).catch((error) => {
this.log.error(`Send cam check to station ${this.rawStation.station_sn} - Error:`, error);
});
}
sendCamCheck2(address, data) {
const payload = (0, utils_1.buildCheckCamPayload2)(this.rawStation.p2p_did, data);
(0, utils_1.sendMessage)(this.socket, address, types_1.RequestMessageType.CHECK_CAM2, payload).catch((error) => {
this.log.error(`Send cam check to station ${this.rawStation.station_sn} - Error:`, error);
});
}
sendPing(address) {
if ((this.lastPong && ((new Date().getTime() - this.lastPong) / this.getHeartbeatInterval() >= this.MAX_RETRIES)) ||
(this.connectTime && !this.lastPong && ((new Date().getTime() - this.connectTime) / this.getHeartbeatInterval() >= this.MAX_RETRIES))) {
if (!this.energySavingDevice)
this.log.warn(`Station ${this.rawStation.station_sn} - Heartbeat check failed. Connection seems lost. Try to reconnect...`);
this._disconnected();
}
(0, utils_1.sendMessage)(this.socket, address, types_1.RequestMessageType.PING).catch((error) => {
this.log.error(`Station ${this.rawStation.station_sn} - Error:`, error);
});
}
sendCommandWithIntString(p2pcommand, customData) {
if (p2pcommand.channel === undefined)
p2pcommand.channel = 0;
if (p2pcommand.value === undefined || typeof p2pcommand.value !== "number")
throw new TypeError("value must be a number");
const payload = (0, utils_1.buildIntStringCommandPayload)(p2pcommand.value, p2pcommand.valueSub === undefined ? 0 : p2pcommand.valueSub, p2pcommand.strValue === undefined ? "" : p2pcommand.strValue, p2pcommand.strValueSub === undefined ? "" : p2pcommand.strValueSub, p2pcommand.channel);
if (p2pcommand.commandType === types_1.CommandType.CMD_NAS_TEST) {
this.currentMessageState[types_1.P2PDataType.DATA].rtspStream[p2pcommand.channel] = p2pcommand.value === 1 ? true : false;
}
this.sendCommand(p2pcommand.commandType, payload, p2pcommand.channel, undefined, customData);
}
sendCommandWithInt(p2pcommand, customData) {
if (p2pcommand.channel === undefined)
p2pcommand.channel = 255;
if (p2pcommand.value === undefined || typeof p2pcommand.value !== "number")
throw new TypeError("value must be a number");
const payload = (0, utils_1.buildIntCommandPayload)(p2pcommand.value, p2pcommand.strValue === undefined ? "" : p2pcommand.strValue, p2pcommand.channel);
this.sendCommand(p2pcommand.commandType, payload, p2pcommand.channel, undefined, customData);
}
sendCommandWithStringPayload(p2pcommand, customData) {
if (p2pcommand.channel === undefined)
p2pcommand.channel = 0;
if (p2pcommand.value === undefined || typeof p2pcommand.value !== "string")
throw new TypeError("value must be a string");
const payload = (0, utils_1.buildCommandWithStringTypePayload)(p2pcommand.value, p2pcommand.channel);
let nested_commandType = undefined;
if (p2pcommand.commandType == types_1.CommandType.CMD_SET_PAYLOAD) {
try {
const json = JSON.parse(p2pcommand.value);
nested_commandType = json.cmd;
}
catch (error) {
this.log.error(`CMD_SET_PAYLOAD - Station ${this.rawStation.station_sn} - Error:`, error);
}
}
else if (p2pcommand.commandType == types_1.CommandType.CMD_DOORBELL_SET_PAYLOAD) {
try {
const json = JSON.parse(p2pcommand.value);
nested_commandType = json.commandType;
}
catch (error) {
this.log.error(`CMD_DOORBELL_SET_PAYLOAD - Station ${this.rawStation.station_sn} - Error:`, error);
}
}
this.sendCommand(p2pcommand.commandType, payload, p2pcommand.channel, nested_commandType, customData);
}
sendCommandWithString(p2pcommand, customData) {
if (p2pcommand.channel === undefined)
p2pcommand.channel = 255;
if (p2pcommand.strValue === undefined)
throw new TypeError("strValue must be defined");
if (p2pcommand.strValueSub === undefined)
throw new TypeError("strValueSub must be defined");
const payload = (0, utils_1.buildStringTypeCommandPayload)(p2pcommand.strValue, p2pcommand.strValueSub, p2pcommand.channel);
this.sendCommand(p2pcommand.commandType, payload, p2pcommand.channel, p2pcommand.commandType, customData);
}
sendCommandPing(channel = 255) {
const payload = (0, utils_1.buildVoidCommandPayload)(channel);
this.sendCommand(types_1.CommandType.CMD_PING, payload, channel);
}
sendCommandDevicePing(channel = 255) {
const payload = (0, utils_1.buildVoidCommandPayload)(channel);
this.sendCommand(types_1.CommandType.CMD_GET_DEVICE_PING, payload, channel);
}
sendCommandWithoutData(commandType, channel = 255) {
const payload = (0, utils_1.buildVoidCommandPayload)(channel);
this.sendCommand(commandType, payload, channel);
}
sendQueuedMessage() {
if (this.sendQueue.length > 0 && this.connected) {
let queuedMessage;
while ((queuedMessage = this.sendQueue.shift()) !== undefined) {
let exists = false;
this.messageStates.forEach(stateMessage => {
if (stateMessage.commandType === queuedMessage.commandType) {
exists = true;
}
});
if (!exists) {
this._sendCommand(queuedMessage);
}
else {
this.sendQueue.unshift(queuedMessage);
break;
}
}
}
else if (!this.connected) {
this.connect();
}
}
sendCommand(commandType, payload, channel, nestedCommandType, customData) {
const message = {
commandType: commandType,
nestedCommandType: nestedCommandType,
channel: channel,
payload: payload,
timestamp: +new Date,
customData: customData
};
this.sendQueue.push(message);
if (message.commandType !== types_1.CommandType.CMD_PING && message.commandType !== types_1.CommandType.CMD_GET_DEVICE_PING)
this._clearESDDisconnectTimeout();
this.sendQueuedMessage();
}
_sendCommand(message) {
var _a;
if ((0, utils_1.isP2PQueueMessage)(message)) {
const ageing = +new Date - message.timestamp;
if (ageing <= this.MAX_COMMAND_QUEUE_TIMEOUT) {
const commandHeader = (0, utils_1.buildCommandHeader)(this.seqNumber, message.commandType);
const data = Buffer.concat([commandHeader, message.payload]);
const messageState = {
sequence: this.seqNumber,
commandType: message.commandType,
nestedCommandType: message.nestedCommandType,
channel: message.channel,
data: data,
retries: 0,
acknowledged: false,
returnCode: types_1.ErrorCode.ERROR_COMMAND_TIMEOUT,
customData: message.customData
};
message = messageState;
this.seqNumber = this._incrementSequence(this.seqNumber);
}
else if (message.commandType === types_1.CommandType.CMD_PING || message.commandType === types_1.CommandType.CMD_GET_DEVICE_PING) {
return;
}
else {
this.log.warn(`Station ${this.rawStation.station_sn} - Command aged out from queue`, { commandType: message.commandType, nestedCommandType: message.nestedCommandType, channel: message.channel, ageing: ageing, maxAgeing: this.MAX_COMMAND_QUEUE_TIMEOUT });
this.emit("command", {
command_type: message.nestedCommandType !== undefined ? message.nestedCommandType : message.commandType,
channel: message.channel,
return_code: types_1.ErrorCode.ERROR_CONNECT_TIMEOUT,
customData: message.customData
});
return;
}
}
else {
if (message.retries < this.MAX_RETRIES && message.returnCode !== types_1.ErrorCode.ERROR_CONNECT_TIMEOUT) {
if (message.returnCode === types_1.ErrorCode.ERROR_FAILED_TO_REQUEST) {
this.messageStates.delete(message.sequence);
message.sequence = this.seqNumber;
message.data.writeUInt16BE(message.sequence, 2);
this.seqNumber = this._incrementSequence(this.seqNumber);
this.messageStates.set(message.sequence, message);
}
message.retries++;
}
else {
this.log.error(`Station ${this.rawStation.station_sn} - Max retries ${(_a = this.messageStates.get(message.sequence)) === null || _a === void 0 ? void 0 : _a.retries} - stop with error ${types_1.ErrorCode[message.returnCode]}`, { sequence: message.sequence, commandType: message.commandType, channel: message.channel, retries: message.retries, returnCode: message.returnCode });
this.emit("command", {
command_type: message.nestedCommandType !== undefined ? message.nestedCommandType : message.commandType,
channel: message.channel,
return_code: message.returnCode,
customData: message.customData
});
this.messageStates.delete(message.sequence);
this.sendQueuedMessage();
return;
}
}
message = message;
message.returnCode = types_1.ErrorCode.ERROR_COMMAND_TIMEOUT;
message.timeout = setTimeout(() => {
this._sendCommand(message);
}, this.MAX_AKNOWLEDGE_TIMEOUT);
this.messageStates.set(message.sequence, message);
if (message.commandType !== types_1.CommandType.CMD_PING && this.energySavingDevice) {
this.energySavingDeviceP2PSeqMapping.set(this.energySavingDeviceP2PDataSeqNumber, message.sequence);
this.log.debug(`Station ${this.rawStation.station_sn} - Energy saving Device - Added sequence number mapping`, { commandType: message.commandType, seqNumber: message.sequence, energySavingDeviceP2PDataSeqNumber: this.energySavingDeviceP2PDataSeqNumber, energySavingDeviceP2PSeqMappingCount: this.energySavingDeviceP2PSeqMapping.size });
this.energySavingDeviceP2PDataSeqNumber = this._incrementSequence(this.energySavingDeviceP2PDataSeqNumber);
}
this.log.debug("Sending p2p command...", { station: this.rawStation.station_sn, sequence: message.sequence, commandType: message.commandType, channel: message.channel, retries: message.retries, messageStatesSize: this.messageStates.size });
(0, utils_1.sendMessage)(this.socket, this.connectAddress, types_1.RequestMessageType.DATA, message.data).catch((error) => {
this.log.error(`Station ${this.rawStation.station_sn} - Error:`, error);
});
if (message.retries === 0) {
if (message.commandType === types_1.CommandType.CMD_START_REALTIME_MEDIA ||
(message.nestedCommandType !== undefined && message.nestedCommandType === types_1.CommandType.CMD_START_REALTIME_MEDIA && message.commandType === types_1.CommandType.CMD_SET_PAYLOAD) ||
message.commandType === types_1.CommandType.CMD_RECORD_VIEW ||
(message.nestedCommandType !== undefined && message.nestedCommandType === 1000 && message.commandType === types_1.CommandType.CMD_DOORBELL_SET_PAYLOAD)) {
if (this.currentMessageState[types_1.P2PDataType.VIDEO].p2pStreaming && message.channel !== this.currentMessageState[types_1.P2PDataType.VIDEO].p2pStreamChannel) {
this.endStream(types_1.P2PDataType.VIDEO);
}
this.currentMessageState[types_1.P2PDataType.VIDEO].p2pStreaming = true;
this.currentMessageState[types_1.P2PDataType.VIDEO].p2pStreamChannel = message.channel;
}
else if (message.commandType === types_1.CommandType.CMD_DOWNLOAD_VIDEO) {
if (this.currentMessageState[types_1.P2PDataType.BINARY].p2pStreaming && message.channel !== this.currentMessageState[types_1.P2PDataType.BINARY].p2pStreamChannel) {
this.endStream(types_1.P2PDataType.BINARY);
}
this.currentMessageState[types_1.P2PDataType.BINARY].p2pStreaming = true;
this.currentMessageState[types_1.P2PDataType.BINARY].p2pStreamChannel = message.channel;
}
else if (message.commandType === types_1.CommandType.CMD_STOP_REALTIME_MEDIA) { //TODO: CommandType.CMD_RECORD_PLAY_CTRL only if stop
this.endStream(types_1.P2PDataType.VIDEO);
}
else if (message.commandType === types_1.CommandType.CMD_DOWNLOAD_CANCEL) {
this.endStream(types_1.P2PDataType.BINARY);
}
else if (message.commandType === types_1.CommandType.CMD_NAS_TEST) {
if (this.currentMessageState[types_1.P2PDataType.DATA].rtspStream[message.channel]) {
this.currentMessageState[types_1.P2PDataType.DATA].rtspStreaming[message.channel] = true;
this.emit("rtsp livestream started", message.channel);
}
else {
this.endRTSPStream(message.channel);
}
}
}
}
handleMsg(msg, rinfo) {
if ((0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.LOCAL_LOOKUP_RESP)) {
if (!this.connected) {
this._clearLookupTimeout();
this._clearLookupRetryTimeout();
this.log.debug(`Station ${this.rawStation.station_sn} - LOCAL_LOOKUP_RESP - Got response`, { ip: rinfo.address, port: rinfo.port });
this._connect({ host: rinfo.address, port: rinfo.port });
}
}
else if ((0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.LOOKUP_ADDR)) {
if (!this.connected) {
const port = msg.slice(6, 8).readUInt16LE();
const ip = `${msg[11]}.${msg[10]}.${msg[9]}.${msg[8]}`;
this.log.debug(`Station ${this.rawStation.station_sn} - LOOKUP_ADDR - Got response`, { remoteAddress: rinfo.address, remotePort: rinfo.port, response: { ip: ip, port: port } });
if (ip === "0.0.0.0") {
this.log.debug(`Station ${this.rawStation.station_sn} - LOOKUP_ADDR - Got invalid ip address 0.0.0.0, ignoring response...`);
return;
}
if ((0, utils_1.isPrivateIp)(ip))
this.localIPAddress = ip;
if (this.connectionType === types_1.P2PConnectionType.ONLY_LOCAL) {
if ((0, utils_1.isPrivateIp)(ip)) {
this._clearLookupTimeout();
this._clearLookupRetryTimeout();
this.log.debug(`Station ${this.rawStation.station_sn} - ONLY_LOCAL - Try to connect to ${ip}:${port}...`);
this._connect({ host: ip, port: port });
}
}
else if (this.connectionType === types_1.P2PConnectionType.QUICKEST) {
this._clearLookupTimeout();
this._clearLookupRetryTimeout();
this.log.debug(`Station ${this.rawStation.station_sn} - QUICKEST - Try to connect to ${ip}:${port}...`);
this._connect({ host: ip, port: port });
}
}
}
else if ((0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.CAM_ID) || (0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.CAM_ID2)) {
// Answer from the device to a CAM_CHECK message
if (!this.connected) {
this.log.debug(`Station ${this.rawStation.station_sn} - CAM_ID - Connected to station ${this.rawStation.station_sn} on host ${rinfo.address} port ${rinfo.port}`);
this._clearLookupRetryTimeout();
this._clearLookupTimeout();
this._clearConnectTimeout();
this.connected = true;
this.connectTime = new Date().getTime();
this.lastPong = null;
this.connectAddress = { host: rinfo.address, port: rinfo.port };
if ((0, utils_1.isPrivateIp)(rinfo.address))
this.localIPAddress = rinfo.address;
this.heartbeatTimeout = setTimeout(() => {
this.scheduleHeartbeat();
}, this.getHeartbeatInterval());
if (this.energySavingDevice) {
this.keepaliveTimeout = setTimeout(() => {
this.scheduleP2PKeepalive();
}, this.KEEPALIVE_INTERVAL);
}
this.emit("connect", this.connectAddress);
if (device_1.Device.isLockWifi(this.rawStation.device_type) || device_1.Device.isLockWifiNoFinger(this.rawStation.device_type)) {
const tmpSendQueue = [...this.sendQueue];
this.sendQueue = [];
this.sendCommandWithoutData(types_1.CommandType.CMD_GATEWAYINFO, 255);
this.sendCommandWithStringPayload({
commandType: types_1.CommandType.CMD_SET_PAYLOAD,
value: JSON.stringify({
"account_id": this.rawStation.member.admin_user_id,
"cmd": types_1.CommandType.P2P_QUERY_STATUS_IN_LOCK,
"mChannel": 0,
"mValue3": 0,
"payload": {
"timezone": this.rawStation.time_zone === undefined || this.rawStation.time_zone === "" ? (0, utils_2.getAdvancedLockTimezone)(this.rawStation.station_sn) : this.rawStation.time_zone,
}
}),
channel: 0
});
tmpSendQueue.forEach(element => {
this.sendQueue.push(element);
});
}
this.sendQueuedMessage();
} /* else {
this.log.debug(`Station ${this.rawStation.station_sn} - CAM_ID - Already connected, ignoring...`);
}*/
}
else if ((0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.PONG)) {
// Response to a ping from our side
this.lastPong = new Date().getTime();
return;
}
else if ((0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.PING)) {
// Response with PONG to keep alive
(0, utils_1.sendMessage)(this.socket, { host: rinfo.address, port: rinfo.port }, types_1.RequestMessageType.PONG).catch((error) => {
this.log.error(`Station ${this.rawStation.station_sn} - Error:`, error);
});
return;
}
else if ((0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.END)) {
// Connection is closed by device
this.log.debug(`Station ${this.rawStation.station_sn} - END - received from host ${rinfo.address}:${rinfo.port}`);
this.onClose();
return;
}
else if ((0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.ACK)) {
// Device ACK a message from our side
// Number of Acks sended in the message
const dataTypeBuffer = msg.slice(4, 6);
const dataType = this.getDataType(dataTypeBuffer);
const numAcksBuffer = msg.slice(6, 8);
const numAcks = numAcksBuffer.readUIntBE(0, numAcksBuffer.length);
for (let i = 1; i <= numAcks; i++) {
const idx = 6 + i * 2;
const seqBuffer = msg.slice(idx, idx + 2);
const ackedSeqNo = seqBuffer.readUIntBE(0, seqBuffer.length);
// -> Message with seqNo was received at the station
this.log.debug(`Station ${this.rawStation.station_sn} - ACK ${types_1.P2PDataType[dataType]} - received from host ${rinfo.address}:${rinfo.port} for sequence ${ackedSeqNo}`);
if (dataType === types_1.P2PDataType.DATA) {
const msg_state = this.messageStates.get(ackedSeqNo);
if (msg_state && !msg_state.acknowledged) {
this._clearTimeout(msg_state.timeout);
if (msg_state.commandType === types_1.CommandType.CMD_PING || msg_state.commandType === types_1.CommandType.CMD_GET_DEVICE_PING) {
this.messageStates.delete(ackedSeqNo);
}
else {
msg_state.acknowledged = true;
msg_state.timeout = setTimeout(() => {
//TODO: Retry command in these case?
this.log.warn(`Station ${this.rawStation.station_sn} - Result data for command not received`, { message: { sequence: msg_state.sequence, commandType: msg_state.commandType, nestedCommandType: msg_state.nestedCommandType, channel: msg_state.channel, acknowledged: msg_state.acknowledged, retries: msg_state.retries, returnCode: msg_state.returnCode, data: msg_state.data } });
this.messageStates.delete(ackedSeqNo);
this.emit("command", {
command_type: msg_state.nestedCommandType !== undefined ? msg_state.nestedCommandType : msg_state.commandType,
channel: msg_state.channel,
return_code: types_1.ErrorCode.ERROR_COMMAND_TIMEOUT,
customData: msg_state.customData
});
this.sendQueuedMessage();
this.closeEnergySavingDevice();
}, this.MAX_COMMAND_RESULT_WAIT);
this.messageStates.set(ackedSeqNo, msg_state);
}
}
}
else if (dataType === types_1.P2PDataType.VIDEO) {
const msg_state = this.messageVideoStates.get(ackedSeqNo);
if (msg_state) {
this._clearTimeout(msg_state.timeout);
this.messageVideoStates.delete(ackedSeqNo);
}
}
}
}
else if ((0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.DATA)) {
if (this.connected) {
const seqNo = msg.slice(6, 8).readUInt16BE();
const dataTypeBuffer = msg.slice(4, 6);
const dataType = this.getDataType(dataTypeBuffer);
const message = {
bytesToRead: msg.slice(2, 4).readUInt16BE(),
type: dataType,
seqNo: seqNo,
data: msg.slice(8)
};
this.sendAck({ host: rinfo.address, port: rinfo.port }, dataTypeBuffer, seqNo);
this.log.debug(`Station ${this.rawStation.station_sn} - DATA ${types_1.P2PDataType[message.type]} - received from host ${rinfo.address}:${rinfo.port} - Processing sequence ${message.seqNo}...`);
if (message.seqNo === this.expectedSeqNo[dataType]) {
// expected seq packet arrived
const timeout = this.currentMessageState[dataType].waitForSeqNoTimeout;
if (!!timeout) {
clearTimeout(timeout);
this.currentMessageState[dataType].waitForSeqNoTimeout = undefined;
}
this.expectedSeqNo[dataType] = this._incrementSequence(this.expectedSeqNo[dataType]);
this.parseDataMessage(message);
this.log.debug(`Station ${this.rawStation.station_sn} - DATA ${types_1.P2PDataType[message.type]} - Received expected sequence (seqNo: ${message.seqNo} queuedData.size: ${this.currentMessageState[dataType].queuedData.size})`);
for (const element of this.currentMessageState[dataType].queuedData.values()) {
if (this.expectedSeqNo[dataType] === element.seqNo) {
this.log.debug(`Station ${this.rawStation.station_sn} - DATA ${types_1.P2PDataType[element.type]} - Work off queued data (seqNo: ${element.seqNo} queuedData.size: ${this.currentMessageState[dataType].queuedData.size})`);
this.expectedSeqNo[dataType]++;
this.parseDataMessage(element);
this.currentMessageState[dataType].queuedData.delete(element.seqNo);
}
else {
this.log.debug(`Station ${this.rawStation.station_sn} - DATA ${types_1.P2PDataType[element.type]} - Work off missing data interrupt queue dismantle (seqNo: ${element.seqNo} queuedData.size: ${this.currentMessageState[dataType].queuedData.size})`);
break;
}
}
}
else if (this.expectedSeqNo[dataType] > message.seqNo) {
// We have already seen this message, skip!
// This can happen because the device is sending the message till it gets a ACK
// which can take some time.
this.log.debug(`Station ${this.rawStation.station_sn} - DATA ${types_1.P2PDataType[message.type]} - Received already processed sequence (seqNo: ${message.seqNo} queuedData.size: ${this.currentMessageState[dataType].queuedData.size})`);
return;
}
else {
if (!this.currentMessageState[dataType].waitForSeqNoTimeout)
this.currentMessageState[dataType].waitForSeqNoTimeout = setTimeout(() => {
//TODO: End stream doesn't stop device for sending video and audio data
this.endStream(dataType);
this.currentMessageState[dataType].waitForSeqNoTimeout = undefined;
}, this.MAX_EXPECTED_SEQNO_WAIT);
if (!this.currentMessageState[dataType].queuedData.get(message.seqNo)) {
this.currentMessageState[dataType].queuedData.set(message.seqNo, message);
this.log.debug(`Station ${this.rawStation.station_sn} - DATA ${types_1.P2PDataType[message.type]} - Received not expected sequence, added to the queue for future processing (seqNo: ${message.seqNo} queuedData.size: ${this.currentMessageState[dataType].queuedData.size})`);
}
else {
this.log.debug(`Station ${this.rawStation.station_sn} - DATA ${types_1.P2PDataType[message.type]} - Received not expected sequence, discarded since already present in queue for future processing (seqNo: ${message.seqNo} queuedData.size: ${this.currentMessageState[dataType].queuedData.size})`);
}
}
}
}
else if ((0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.LOOKUP_ADDR2)) {
if (!this.connected) {
const port = msg.slice(6, 8).readUInt16LE();
const ip = `${msg[11]}.${msg[10]}.${msg[9]}.${msg[8]}`;
const data = msg.slice(20, 24);
this._clearLookupTimeout();
this._clearLookupRetryTimeout();
this.log.debug(`Station ${this.rawStation.station_sn} - LOOKUP_ADDR2 - Got response`, { remoteAddress: rinfo.address, remotePort: rinfo.port, response: { ip: ip, port: port, data: data.toString("hex") } });
this.log.debug(`Station ${this.rawStation.station_sn} - CHECK_CAM2 - Connecting to host ${ip} on port ${port}...`);
for (let i = 0; i < 4; i++)
this.sendCamCheck2({ host: ip, port: port }, data);
this._startConnectTimeout();
(0, utils_1.sendMessage)(this.socket, { host: ip, port: port }, types_1.RequestMessageType.UNKNOWN_70).catch((error) => {
this.log.error(`Station ${this.rawStation.station_sn} - UNKNOWN_70 - Error:`, error);
});
}
}
else if ((0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.UNKNOWN_71)) {
if (!this.connected) {
this.log.debug(`Station ${this.rawStation.station_sn} - UNKNOWN_71 - Got response`, { remoteAddress: rinfo.address, remotePort: rinfo.port, response: { message: msg.toString("hex"), length: msg.length } });
(0, utils_1.sendMessage)(this.socket, { host: rinfo.address, port: rinfo.port }, types_1.RequestMessageType.UNKNOWN_71).catch((error) => {
this.log.error(`Station ${this.rawStation.station_sn} - UNKNOWN_71 - Error:`, error);
});
}
}
else if ((0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.UNKNOWN_73)) {
if (!this.connected) {
const port = msg.slice(8, 10).readUInt16BE();
const data = msg.slice(4, 8);
this.log.debug(`Station ${this.rawStation.station_sn} - UNKNOWN_73 - Got response`, { remoteAddress: rinfo.address, remotePort: rinfo.port, response: { port: port, data: data.toString("hex") } });
this.cloudLookup2({ host: rinfo.address, port: port }, data);
}
}
else if ((0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.UNKNOWN_81) || (0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.UNKNOWN_83)) {
// Do nothing / ignore
}
else if ((0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.LOOKUP_RESP)) {
if (!this.connected) {
const responseCode = msg.slice(4, 6).readUInt16LE();
this.log.debug(`Station ${this.rawStation.station_sn} - LOOKUP_RESP - Got response`, { remoteAddress: rinfo.address, remotePort: rinfo.port, response: { responseCode: responseCode } });
/*if (responseCode !== 0 && this.lookupTimeout !== undefined && this.lookupRetryTimeout === undefined) {
this.lookupRetryTimeout = setTimeout(() => {
this.lookupRetryTimeout = undefined;
this.cloudAddresses.map((address) => this.cloudLookupByAddress(address));
}, this.LOOKUP_RETRY_TIMEOUT);
}*/
}
}
else {
this.log.debug(`Station ${this.rawStation.station_sn} - received unknown message`, { remoteAddress: rinfo.address, remotePort: rinfo.port, response: { message: msg.toString("hex"), length: msg.length } });
}
}
parseDataMessage(message) {
if ((message.type === types_1.P2PDataType.BINARY || message.type === types_1.P2PDataType.VIDEO) && !this.currentMessageState[message.type].p2pStreaming) {
this.log.debug(`Station ${this.rawStation.station_sn} - DATA ${types_1.P2PDataType[message.type]} - Stream not started ignore this data`, { seqNo: message.seqNo, header: this.currentMessageBuilder[message.type].header, bytesRead: this.currentMessageBuilder[message.type].bytesRead, bytesToRead: this.currentMessageBuilder[message.type].header.bytesToRead, messageSize: message.data.length });
}
else {
if (this.currentMessageState[message.type].leftoverData.length > 0) {
message.data = Buffer.concat([this.currentMessageState[message.type].leftoverData, message.data]);
this.currentMessageState[message.type].leftoverData = Buffer.from([]);
}
let data = message.data;
do {
// is this the first message?
const firstPartMessage = data.slice(0, 4).toString() === utils_1.MAGIC_WORD;
if (firstPartMessage) {
const header = {
commandId: 0,
bytesToRead: 0,
channel: 0,
signCode: 0,
type: 0
};
header.commandId = data.slice(4, 6).readUIntLE(0, 2);
header.bytesToRead = data.slice(6, 10).readUIntLE(0, 4);
header.channel = data.slice(12, 13).readUInt8();
header.signCode = data.slice(13, 14).readInt8();
header.type = data.slice(14, 15).readUInt8();
this.currentMessageBuilder[message.type].header = header;
data = data.slice(this.P2P_DATA_HEADER_BYTES);
if (data.length >= header.bytesToRead) {
const payload = data.slice(0, header.bytesToRead);
this.currentMessageBuilder[message.type].messages[message.seqNo] = payload;
this.currentMessageBuilder[message.type].bytesRead = payload.byteLength;
data = data.slice(header.bytesToRead);
if (data.length <= this.P2P_DATA_HEADER_BYTES) {
this.currentMessageState[message.type].leftoverData = data;
data = Buffer.from([]);
}
}
else {