UNPKG

eufy-security-client-fork

Version:

Client to comunicate with Eufy-Security devices

878 lines (877 loc) 117 kB
"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 {