UNPKG

@spectralblu/eufy-security-client

Version:

Client to comunicate with Eufy-Security devices, forked from bropat/eufy-client-security

921 lines 168 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; 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 date_and_time_1 = __importDefault(require("date-and-time")); 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"); const ble_1 = require("./ble"); const http_1 = require("../http"); const utils_3 = require("../utils"); class P2PClientProtocol extends tiny_typed_emitter_1.TypedEmitter { MAX_RETRIES = 10; MAX_COMMAND_RESULT_WAIT = 30 * 1000; MAX_AKNOWLEDGE_TIMEOUT = 15 * 1000; MAX_LOOKUP_TIMEOUT = 15 * 1000; LOOKUP_RETRY_TIMEOUT = 3 * 1000; LOOKUP2_TIMEOUT = 5 * 1000; MAX_EXPECTED_SEQNO_WAIT = 20 * 1000; HEARTBEAT_INTERVAL = 5 * 1000; MAX_COMMAND_QUEUE_TIMEOUT = 120 * 1000; AUDIO_CODEC_ANALYZE_TIMEOUT = 650; KEEPALIVE_INTERVAL = 5 * 1000; ESD_DISCONNECT_TIMEOUT = 30 * 1000; MAX_STREAM_DATA_WAIT = 5 * 1000; RESEND_NOT_ACKNOWLEDGED_COMMAND = 100; UDP_RECVBUFFERSIZE_BYTES = 1048576; MAX_PAYLOAD_BYTES = 1028; MAX_PACKET_BYTES = 1024; MAX_VIDEO_PACKET_BYTES = 655360; P2P_DATA_HEADER_BYTES = 16; MAX_SEQUENCE_NUMBER = 65535; /* * SEQUENCE_PROCESSING_BOUNDARY is used to determine if an incoming sequence number * that is lower than the expected one was already processed. * If it is within the boundary, it is determined as 'already processed', * If it is even lower, it is assumed that the sequence count has reached * MAX_SEQUENCE_NUMBER and restarted at 0. * */ SEQUENCE_PROCESSING_BOUNDARY = 20000; // worth of approx. 90 seconds of continous streaming socket; binded = false; connected = false; connecting = false; terminating = false; handshake_UNKNOWN71 = false; seqNumber = 0; offsetDataSeqNumber = 0; videoSeqNumber = 0; lockSeqNumber = -1; expectedSeqNo = {}; currentMessageBuilder = {}; currentMessageState = {}; talkbackStream; downloadTotalBytes = 0; downloadReceivedBytes = 0; cloudAddresses; messageStates = new sweet_collections_1.SortedMap((a, b) => a - b); messageVideoStates = new sweet_collections_1.SortedMap((a, b) => a - b); sendQueue = new Array(); connectTimeout; lookupTimeout; lookupRetryTimeout; lookup2Timeout; heartbeatTimeout; keepaliveTimeout; esdDisconnectTimeout; secondaryCommandTimeout; connectTime = null; lastPong = null; lastPongData = undefined; connectionType = types_1.P2PConnectionType.QUICKEST; energySavingDevice = false; p2pSeqMapping = new Map(); p2pDataSeqNumber = 0; connectAddress = undefined; localIPAddress = undefined; preferredIPAddress = undefined; dskKey = ""; dskExpiration = null; log; deviceSNs = {}; api; rawStation; lastCustomData; lastChannel; lockPublicKey; lockAESKeys = new Map(); channel = 255; encryption = types_1.EncryptionType.NONE; p2pKey; constructor(rawStation, api, ipAddress, publicKey = "") { super(); this.api = api; this.lockPublicKey = publicKey; this.preferredIPAddress = ipAddress; this.log = api.getLog(); this.cloudAddresses = (0, utils_1.decodeP2PCloudIPs)(rawStation.app_conn); this.log.debug("Loaded P2P cloud ip addresses", { stationSN: rawStation.station_sn, ipAddress: ipAddress, cloudAddresses: 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; } _isBetween(n, lowBoundary, highBoundary) { if (n < lowBoundary) return false; if (n >= highBoundary) return false; return true; } _wasSequenceNumberAlreadyProcessed(expectedSequence, receivedSequence) { if ((expectedSequence - this.SEQUENCE_PROCESSING_BOUNDARY) > 0) { // complete boundary without squence number reset return this._isBetween(receivedSequence, expectedSequence - this.SEQUENCE_PROCESSING_BOUNDARY, expectedSequence); } else { // there was a sequence number reset recently const isInRangeAfterReset = this._isBetween(receivedSequence, 0, expectedSequence); const isInRangeBeforeReset = this._isBetween(receivedSequence, this.MAX_SEQUENCE_NUMBER + (expectedSequence - this.SEQUENCE_PROCESSING_BOUNDARY), this.MAX_SEQUENCE_NUMBER); return (isInRangeBeforeReset || isInRangeAfterReset); } } _initialize() { let rsaKey; this.connected = false; this.handshake_UNKNOWN71 = false; this.connecting = false; this.lastPong = null; this.lastPongData = undefined; this.connectTime = null; this.seqNumber = 0; this.offsetDataSeqNumber = 0; this.videoSeqNumber = 0; this.p2pDataSeqNumber = 0; this.lockSeqNumber = -1; this.connectAddress = undefined; this.lastChannel = undefined; this.lastCustomData = undefined; this.encryption = types_1.EncryptionType.NONE; this.p2pKey = undefined; this.lockAESKeys.clear(); this._clearMessageStateTimeouts(); this._clearMessageVideoStateTimeouts(); this.messageStates.clear(); this.messageVideoStates.clear(); this.p2pSeqMapping.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; } _clearLookup2Timeout() { this._clearTimeout(this.lookup2Timeout); this.lookup2Timeout = undefined; } _clearESDDisconnectTimeout() { this._clearTimeout(this.esdDisconnectTimeout); this.esdDisconnectTimeout = undefined; } _clearSecondaryCommandTimeout() { this._clearTimeout(this.secondaryCommandTimeout); this.secondaryCommandTimeout = undefined; } async sendMessage(errorSubject, address, msgID, payload) { await (0, utils_1.sendMessage)(this.socket, address, msgID, payload).catch((err) => { const error = (0, error_1.ensureError)(err); this.log.error(`${errorSubject} - Error`, { error: (0, utils_3.getError)(error), stationSN: this.rawStation.station_sn, address: address, msgID: msgID.toString("hex"), payload: payload?.toString("hex") }); }); } _disconnected() { this._clearHeartbeatTimeout(); this._clearKeepaliveTimeout(); this._clearLookupRetryTimeout(); this._clearLookup2Timeout(); this._clearLookupTimeout(); this._clearConnectTimeout(); this._clearESDDisconnectTimeout(); this._clearSecondaryCommandTimeout(); this._clearMessageStateTimeouts(); this._clearMessageVideoStateTimeouts(); 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.p2pCommand.commandType !== types_1.CommandType.CMD_PING && queue.p2pCommand.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.p2pCommand.commandType !== types_1.CommandType.CMD_PING && queue.p2pCommand.commandType !== types_1.CommandType.CMD_GET_DEVICE_PING).length === 0 && this.energySavingDevice && !this.isCurrentlyStreaming() && Array.from(this.messageStates.values()).filter((msgState) => msgState.acknowledged === false).length === 0) { if (this.esdDisconnectTimeout === undefined) { this.log.debug(`Energy saving device - No more p2p commands to execute or running streams, initiate disconnect timeout in ${this.ESD_DISCONNECT_TIMEOUT} milliseconds...`, { stationSN: this.rawStation.station_sn }); this.esdDisconnectTimeout = setTimeout(() => { this.esdDisconnectTimeout = undefined; this.sendMessage(`Closing of connection for battery saving`, this.connectAddress, types_1.RequestMessageType.END); 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(`DSK keys not present or expired, get/renew it`, { stationSN: this.rawStation.station_sn, 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.lookup2Timeout = setTimeout(() => { this.cloudLookup2(); }, this.LOOKUP2_TIMEOUT); } cloudLookup2() { this.cloudAddresses.map((address) => this.cloudLookupByAddress2(address)); } cloudLookup3(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]); await this.sendMessage(`Local lookup address`, address, msgId, payload); } 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); await this.sendMessage(`Cloud lookup addresses`, address, msgId, payload); } 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); await this.sendMessage(`Cloud lookup addresses (2)`, address, msgId, payload); } async 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); await this.sendMessage(`Cloud lookup addresses (3)`, address, msgId, payload); } isConnected() { return this.connected; } _startConnectTimeout() { if (this.connectTimeout === undefined) this.connectTimeout = setTimeout(() => { this.log.warn(`Tried all hosts, no connection could be established to station ${this.rawStation.station_sn}.`); this._disconnected(); }, this.MAX_AKNOWLEDGE_TIMEOUT); } _connect(address, p2p_did) { this.log.debug(`Connecting to host ${address.host} on port ${address.port} (CHECK_CAM)`, { stationSN: this.rawStation.station_sn, address: address, p2pDid: p2p_did }); for (let i = 0; i < 4; i++) this.sendCamCheck(address, p2p_did); this._startConnectTimeout(); } lookup(host) { if (host === undefined) { if (this.preferredIPAddress !== undefined) { host = this.preferredIPAddress; } else if (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(`All address lookup tentatives failed.`, { stationSN: this.rawStation.station_sn }); 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); this.socket.setBroadcast(true); } catch (err) { const error = (0, error_1.ensureError)(err); this.log.error(`connect - Error`, { error: (0, utils_3.getError)(error), stationSN: this.rawStation.station_sn, host: host, currentRecBufferSize: this.socket.getRecvBufferSize(), recBufferRequestedSize: this.UDP_RECVBUFFERSIZE_BYTES }); } this.lookup(host); }); else { this.lookup(host); } } } async sendCamCheck(address, p2p_did) { const payload = (0, utils_1.buildCheckCamPayload)(p2p_did); await this.sendMessage(`Send cam check`, address, types_1.RequestMessageType.CHECK_CAM, payload); } async sendCamCheck2(address, data) { const payload = (0, utils_1.buildCheckCamPayload2)(this.rawStation.p2p_did, data); await this.sendMessage(`Send cam check (2)`, address, types_1.RequestMessageType.CHECK_CAM2, payload); } async 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(`Heartbeat check failed for station ${this.rawStation.station_sn}. Connection seems lost. Try to reconnect...`); this._disconnected(); } await this.sendMessage(`Send ping`, address, types_1.RequestMessageType.PING, this.lastPongData); } 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"); this.sendCommand(p2pcommand, types_1.InternalP2PCommandType.WithIntString, undefined, customData); } sendCommandWithInt(p2pcommand, customData) { if (p2pcommand.channel === undefined) p2pcommand.channel = this.channel; if (p2pcommand.value === undefined || typeof p2pcommand.value !== "number") throw new TypeError("value must be a number"); this.sendCommand(p2pcommand, types_1.InternalP2PCommandType.WithInt, 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"); let nested_commandType = undefined; this.log.debug(`sendCommandWithStringPayload:`, { p2pcommand: p2pcommand, customData: customData }); if (p2pcommand.commandType == types_1.CommandType.CMD_SET_PAYLOAD) { try { const json = JSON.parse(p2pcommand.value); nested_commandType = json.cmd; } catch (err) { const error = (0, error_1.ensureError)(err); this.log.error(`sendCommandWithStringPayload CMD_SET_PAYLOAD - Error`, { error: (0, utils_3.getError)(error), stationSN: this.rawStation.station_sn, p2pcommand: p2pcommand, customData: customData }); } } else if (p2pcommand.commandType == types_1.CommandType.CMD_DOORBELL_SET_PAYLOAD) { try { const json = JSON.parse(p2pcommand.value); nested_commandType = json.commandType; } catch (err) { const error = (0, error_1.ensureError)(err); this.log.error(`sendCommandWithStringPayload CMD_DOORBELL_SET_PAYLOAD - Error`, { error: (0, utils_3.getError)(error), stationSN: this.rawStation.station_sn, p2pcommand: p2pcommand, customData: customData }); } } this.sendCommand(p2pcommand, types_1.InternalP2PCommandType.WithStringPayload, nested_commandType, customData); } sendCommandWithString(p2pcommand, customData) { if (p2pcommand.channel === undefined) p2pcommand.channel = this.channel; if (p2pcommand.strValue === undefined) throw new TypeError("strValue must be defined"); if (p2pcommand.strValueSub === undefined) throw new TypeError("strValueSub must be defined"); this.sendCommand(p2pcommand, types_1.InternalP2PCommandType.WithString, p2pcommand.commandType, customData); } sendCommandPing(channel = this.channel) { this.sendCommand({ commandType: types_1.CommandType.CMD_PING, channel: channel }, types_1.InternalP2PCommandType.WithoutData); } sendCommandDevicePing(channel = this.channel) { this.sendCommand({ commandType: types_1.CommandType.CMD_GET_DEVICE_PING, channel: channel }, types_1.InternalP2PCommandType.WithoutData); } sendCommandWithoutData(commandType, channel = this.channel) { this.sendCommand({ commandType: commandType, channel: channel }, types_1.InternalP2PCommandType.WithoutData); } sendQueuedMessage() { if (this.sendQueue.length > 0) { if (this.connected) { let queuedMessage; while ((queuedMessage = this.sendQueue.shift()) !== undefined) { let exists = false; let waitingAcknowledge = false; this.messageStates.forEach(stateMessage => { if (stateMessage.commandType === queuedMessage.p2pCommand.commandType && stateMessage.nestedCommandType === queuedMessage.nestedCommandType && !stateMessage.acknowledged) { exists = true; } if (!stateMessage.acknowledged || stateMessage.commandType === types_1.CommandType.CMD_GATEWAYINFO) { waitingAcknowledge = true; } }); if (!exists && !waitingAcknowledge) { this._sendCommand(queuedMessage); break; } else { this.sendQueue.unshift(queuedMessage); break; } } } else if (!this.connected) { this.connect(); } } this.closeEnergySavingDevice(); } sendCommand(p2pcommand, p2pcommandType, nestedCommandType, customData) { const message = { p2pCommand: p2pcommand, nestedCommandType: nestedCommandType, timestamp: +new Date, customData: customData, p2pCommandType: p2pcommandType }; this.sendQueue.push(message); if (p2pcommand.commandType !== types_1.CommandType.CMD_PING && p2pcommand.commandType !== types_1.CommandType.CMD_GET_DEVICE_PING) this._clearESDDisconnectTimeout(); this.sendQueuedMessage(); } resendNotAcknowledgedCommand(sequence) { const messageState = this.messageStates.get(sequence); if (messageState) { messageState.retryTimeout = setTimeout(() => { if (this.connectAddress) { (0, utils_1.sendMessage)(this.socket, this.connectAddress, types_1.RequestMessageType.DATA, messageState.data).catch((err) => { const error = (0, error_1.ensureError)(err); this.log.error(`resendNotAcknowledgedCommand - Error`, { error: (0, utils_3.getError)(error), stationSN: this.rawStation.station_sn, sequence: sequence }); }); this.resendNotAcknowledgedCommand(sequence); } }, this.RESEND_NOT_ACKNOWLEDGED_COMMAND); } } async _sendCommand(message) { 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.p2pCommand.commandType); let payload; const channel = message.p2pCommand.channel !== undefined ? message.p2pCommand.channel : 0; switch (message.p2pCommandType) { case types_1.InternalP2PCommandType.WithInt: payload = (0, utils_1.buildIntCommandPayload)(this.encryption, this.p2pKey, this.rawStation.station_sn, this.rawStation.p2p_did, message.p2pCommand.commandType, message.p2pCommand.value, message.p2pCommand.strValue === undefined ? "" : message.p2pCommand.strValue, channel); break; case types_1.InternalP2PCommandType.WithIntString: payload = (0, utils_1.buildIntStringCommandPayload)(this.encryption, this.p2pKey, this.rawStation.station_sn, this.rawStation.p2p_did, message.p2pCommand.commandType, message.p2pCommand.value, message.p2pCommand.valueSub === undefined ? 0 : message.p2pCommand.valueSub, message.p2pCommand.strValue === undefined ? "" : message.p2pCommand.strValue, message.p2pCommand.strValueSub === undefined ? "" : message.p2pCommand.strValueSub, channel); //TODO: Check if this "if" can be moved elsewhere if (message.p2pCommand.commandType === types_1.CommandType.CMD_NAS_TEST) { this.currentMessageState[types_1.P2PDataType.DATA].rtspStream[channel] = message.p2pCommand.value === 1 ? true : false; } break; case types_1.InternalP2PCommandType.WithString: payload = (0, utils_1.buildStringTypeCommandPayload)(this.encryption, this.p2pKey, this.rawStation.station_sn, this.rawStation.p2p_did, message.p2pCommand.commandType, message.p2pCommand.strValue, message.p2pCommand.strValueSub, channel); break; case types_1.InternalP2PCommandType.WithStringPayload: payload = (0, utils_1.buildCommandWithStringTypePayload)(this.encryption, this.p2pKey, this.rawStation.station_sn, this.rawStation.p2p_did, message.p2pCommand.commandType, message.p2pCommand.value, channel); break; default: payload = (0, utils_1.buildVoidCommandPayload)(channel); break; } const data = Buffer.concat([commandHeader, payload]); const messageState = { sequence: this.seqNumber, commandType: message.p2pCommand.commandType, nestedCommandType: message.nestedCommandType, channel: 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.p2pCommand.commandType === types_1.CommandType.CMD_PING || message.p2pCommand.commandType === types_1.CommandType.CMD_GET_DEVICE_PING) { return; } else { this.log.warn(`Command aged out from send queue for station ${this.rawStation.station_sn}`, { commandType: message.p2pCommand.commandType, nestedCommandType: message.nestedCommandType, channel: message.p2pCommand.channel, ageing: ageing, maxAgeing: this.MAX_COMMAND_QUEUE_TIMEOUT }); this.emit("command", { command_type: message.nestedCommandType !== undefined ? message.nestedCommandType : message.p2pCommand.commandType, channel: message.p2pCommand.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(`Max p2p command send retries reached.`, { stationSN: this.rawStation.station_sn, 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; } } const messageState = message; messageState.returnCode = types_1.ErrorCode.ERROR_COMMAND_TIMEOUT; messageState.timeout = setTimeout(() => { this._clearTimeout(messageState.retryTimeout); this._sendCommand(messageState); this._clearESDDisconnectTimeout(); this.closeEnergySavingDevice(); }, this.MAX_AKNOWLEDGE_TIMEOUT); this.messageStates.set(messageState.sequence, messageState); messageState.retryTimeout = setTimeout(() => { this.resendNotAcknowledgedCommand(messageState.sequence); }, this.RESEND_NOT_ACKNOWLEDGED_COMMAND); if (messageState.commandType !== types_1.CommandType.CMD_PING && messageState.commandType !== types_1.CommandType.CMD_GATEWAYINFO) { this.p2pSeqMapping.set(this.p2pDataSeqNumber, message.sequence); this.log.debug(`Added sequence number mapping`, { stationSN: this.rawStation.station_sn, commandType: message.commandType, seqNumber: message.sequence, p2pDataSeqNumber: this.p2pDataSeqNumber, p2pSeqMappingCount: this.p2pSeqMapping.size }); this.p2pDataSeqNumber = this._incrementSequence(this.p2pDataSeqNumber); } this.log.debug("Sending p2p command...", { station: this.rawStation.station_sn, sequence: messageState.sequence, commandType: messageState.commandType, channel: messageState.channel, retries: messageState.retries, messageStatesSize: this.messageStates.size }); await this.sendMessage(`Send p2p command`, this.connectAddress, types_1.RequestMessageType.DATA, messageState.data); if (messageState.retries === 0) { if (messageState.commandType === types_1.CommandType.CMD_START_REALTIME_MEDIA || (messageState.nestedCommandType !== undefined && messageState.nestedCommandType === types_1.CommandType.CMD_START_REALTIME_MEDIA && messageState.commandType === types_1.CommandType.CMD_SET_PAYLOAD) || messageState.commandType === types_1.CommandType.CMD_RECORD_VIEW || (messageState.nestedCommandType !== undefined && messageState.nestedCommandType === 1000 && messageState.commandType === types_1.CommandType.CMD_DOORBELL_SET_PAYLOAD)) { if (this.currentMessageState[types_1.P2PDataType.VIDEO].p2pStreaming && messageState.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 = messageState.channel; } else if (messageState.commandType === types_1.CommandType.CMD_DOWNLOAD_VIDEO) { if (this.currentMessageState[types_1.P2PDataType.BINARY].p2pStreaming && messageState.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 (messageState.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 (messageState.commandType === types_1.CommandType.CMD_DOWNLOAD_CANCEL) { this.endStream(types_1.P2PDataType.BINARY); } else if (messageState.commandType === types_1.CommandType.CMD_NAS_TEST) { if (this.currentMessageState[types_1.P2PDataType.DATA].rtspStream[messageState.channel]) { this.currentMessageState[types_1.P2PDataType.DATA].rtspStreaming[messageState.channel] = true; this.emit("rtsp livestream started", messageState.channel); } else { this.endRTSPStream(messageState.channel); } } } } handleMsg(msg, rinfo) { if ((0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.LOCAL_LOOKUP_RESP)) { if (!this.connected) { this._clearLookupTimeout(); this._clearLookupRetryTimeout(); const p2pDid = `${msg.subarray(4, 12).toString("utf8").replace(/[\0]+$/g, "")}-${msg.subarray(12, 16).readUInt32BE().toString().padStart(6, "0")}-${msg.subarray(16, 24).toString("utf8").replace(/[\0]+$/g, "")}`; this.log.debug(`Received message - LOCAL_LOOKUP_RESP - Got response`, { stationSN: this.rawStation.station_sn, ip: rinfo.address, port: rinfo.port, p2pDid: p2pDid }); if (p2pDid === this.rawStation.p2p_did) { this.log.debug(`Received message - LOCAL_LOOKUP_RESP - Wanted device was found, connect to it`, { stationSN: this.rawStation.station_sn, ip: rinfo.address, port: rinfo.port, p2pDid: p2pDid }); this._connect({ host: rinfo.address, port: rinfo.port }, p2pDid); } else { this.log.debug(`Received message - LOCAL_LOOKUP_RESP - Unwanted device was found, don't connect to it`, { stationSN: this.rawStation.station_sn, ip: rinfo.address, port: rinfo.port, p2pDid: p2pDid }); } } } else if ((0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.LOOKUP_ADDR)) { if (!this.connected) { const port = msg.subarray(6, 8).readUInt16LE(); const ip = `${msg[11]}.${msg[10]}.${msg[9]}.${msg[8]}`; this.log.debug(`Received message - LOOKUP_ADDR - Got response`, { stationSN: this.rawStation.station_sn, remoteAddress: rinfo.address, remotePort: rinfo.port, response: { ip: ip, port: port } }); if (ip === "0.0.0.0") { this.log.debug(`Received message - LOOKUP_ADDR - Got invalid ip address 0.0.0.0, ignoring response...`, { stationSN: this.rawStation.station_sn, remoteAddress: rinfo.address, remotePort: rinfo.port, response: { ip: ip, port: port } }); 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(`Trying to connect in ONLY_LOCAL mode...`, { stationSN: this.rawStation.station_sn, ip: ip, port: port }); this._connect({ host: ip, port: port }, this.rawStation.p2p_did); } } else if (this.connectionType === types_1.P2PConnectionType.QUICKEST) { this._clearLookupTimeout(); this._clearLookupRetryTimeout(); this.log.debug(`Trying to connect in QUICKEST mode...`, { stationSN: this.rawStation.station_sn, ip: ip, port: port }); this._connect({ host: ip, port: port }, this.rawStation.p2p_did); } } } 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(`Received message - CAM_ID - Connected to station ${this.rawStation.station_sn} on host ${rinfo.address} port ${rinfo.port}`); this._clearLookupRetryTimeout(); this._clearLookupTimeout(); this._clearConnectTimeout(); this._clearLookup2Timeout(); this.connected = true; this.connectTime = new Date().getTime(); this.lastPong = null; this.lastPongData = undefined; 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); } 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); }); } else if (device_1.Device.isSmartSafe(this.rawStation.device_type)) { const payload = (0, utils_1.buildVoidCommandPayload)(255); const data = Buffer.concat([(0, utils_1.buildCommandHeader)(this.seqNumber, types_1.CommandType.CMD_GATEWAYINFO), payload]); const message = { sequence: this.seqNumber, commandType: types_1.CommandType.CMD_GATEWAYINFO, channel: 255, data: data, retries: 0, acknowledged: false, returnCode: types_1.ErrorCode.ERROR_COMMAND_TIMEOUT, }; this.messageStates.set(message.sequence, message); message.retryTimeout = setTimeout(() => { this.resendNotAcknowledgedCommand(message.sequence); }, this.RESEND_NOT_ACKNOWLEDGED_COMMAND); this.seqNumber = this._incrementSequence(this.seqNumber); this.sendMessage(`Send smartsafe gateway command to station`, this.connectAddress, types_1.RequestMessageType.DATA, data); const tmpSendQueue = [...this.sendQueue]; this.sendQueue = []; this.sendCommandPing(); tmpSendQueue.forEach(element => { this.sendQueue.push(element); }); } else if (device_1.Device.isLockWifiR10(this.rawStation.device_type) || device_1.Device.isLockWifiR20(this.rawStation.device_type)) { const tmpSendQueue = [...this.sendQueue]; this.sendQueue = []; const payload = (0, utils_1.buildVoidCommandPayload)(255); const data = Buffer.concat([(0, utils_1.buildCommandHeader)(0, types_1.CommandType.CMD_GATEWAYINFO), payload.subarray(0, payload.length - 2), (0, utils_1.buildCommandHeader)(0, types_1.CommandType.CMD_PING).subarray(2), payload]); const message = { sequence: this.seqNumber, commandType: types_1.CommandType.CMD_PING, channel: 255, data: data, retries: 0, acknowledged: false, returnCode: types_1.ErrorCode.ERROR_COMMAND_TIMEOUT, }; this.messageStates.set(message.sequence, message); message.retryTimeout = setTimeout(() => { this.resendNotAcknowledgedCommand(message.sequence); }, this.RESEND_NOT_ACKNOWLEDGED_COMMAND); this.seqNumber = this._incrementSequence(this.seqNumber); this.sendMessage(`Send lock wifi gateway command to station`, this.connectAddress, types_1.RequestMessageType.DATA, data); try { const command = (0, utils_1.getLockV12P2PCommand)(this.rawStation.station_sn, this.rawStation.member.admin_user_id, types_1.ESLCommand.QUERY_STATUS_IN_LOCK, 0, this.lockPublicKey, this.incLockSequenceNumber(), device_1.Lock.encodeCmdStatus(this.rawStation.member.admin_user_id)); this.sendCommandWithStringPayload(command.payload); } catch (err) { const error = (0, error_1.ensureError)(err); this.log.error(`Send query status lock command to station - Error`, { error: (0, utils_3.getError)(error), stationSN: this.rawStation.station_sn }); } tmpSendQueue.forEach(element => { this.sendQueue.push(element); }); } else { const tmpSendQueue = [...this.sendQueue]; this.sendQueue = []; this.sendCommandWithoutData(types_1.CommandType.CMD_GATEWAYINFO, http_1.Station.CHANNEL); tmpSendQueue.forEach(element => { this.sendQueue.push(element); }); } this.sendQueuedMessage(); this.emit("connect", this.connectAddress); } } else if ((0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.PONG)) { // Response to a ping from our side this.lastPong = new Date().getTime(); if (msg.length > 4) this.lastPongData = msg.subarray(4); else this.lastPongData = undefined; return; } else if ((0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.PING)) { // Response with PONG to keep alive this.sendMessage(`Send pong`, { host: rinfo.address, port: rinfo.port }, types_1.RequestMessageType.PONG); return; } else if ((0, utils_1.hasHeader)(msg, types_1.ResponseMessageType.END)) { // Connection is closed by device this.log.debug(`Received message - END`, { stationSN: this.rawStation.station_sn, remoteAddress: rinfo.address, remotePort: 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.subarray(4, 6); const dataType = this.getDataType(dataTypeBuffer); const numAcksBuffer = msg.subarray(6, 8); const numAcks = numAcksBuffer.readUIntBE(0, numAcksBuffer.length); for (let i = 1; i <= numAcks; i++) { const idx = 6 + i * 2; const seqBuffer = msg.subarray(idx, idx + 2); const ackedSeqNo = seqBuffer.readUIntBE(0, seqBuffer.length); // -> Message with seqNo was received at the station this.log.debug(`Received message - ACK ${types_1.P2PDataType[dataType]} - sequence ${ackedSeqNo}`, { stationSN: this.rawStation.station_sn, remoteAddress: rinfo.address, remotePort: rinfo.port, ackedSeqNo: ackedSeqNo, dataType: types_1.P2PDataType[dataType] }); if (dataType === types_1.P2PDataType.DATA) { const msg_state = this.messageStates.get(ackedSeqNo); if (msg_state && !msg_state.acknowledged) { this._clearTimeout(msg_state.retryTimeout); 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); this.sendQueuedMessage(); } else { msg_state.acknowledged = true; msg_state.timeout = setTimeout(() => { //TODO: Retry command in these case? this.messageStates.delete(ackedSeqNo); if (msg_state.commandType !== types_1.CommandType.CMD_GATEWAYINFO) { this.log.warn(`Result data for command not received`, { stationSN: this.rawStation.station_sn, 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.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 }); } else { this.log.debug(`Result data for command CMD_GATEWAYINFO not received`, { stationSN: this.rawStation.station_sn, 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.sendQueuedMessage(); }, this.MAX_COMMAND