UNPKG

eufy-security-client

Version:

Client to comunicate with Eufy-Security devices

922 lines 198 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"); const logging_1 = require("../logging"); class P2PClientProtocol extends tiny_typed_emitter_1.TypedEmitter { MAX_RETRIES = 10; MAX_COMMAND_RESULT_WAIT = 30 * 1000; MAX_GATEWAY_COMMAND_RESULT_WAIT = 5 * 1000; MAX_CONNECTION_TIMEOUT = 25 * 1000; MAX_AKNOWLEDGE_TIMEOUT = 5 * 1000; MAX_LOOKUP_TIMEOUT = 20 * 1000; LOCAL_LOOKUP_RETRY_TIMEOUT = 1 * 1000; LOOKUP_RETRY_TIMEOUT = 1 * 1000; LOOKUP2_TIMEOUT = 3 * 1000; LOOKUP2_RETRY_TIMEOUT = 1 * 1000; MAX_EXPECTED_SEQNO_WAIT = 20 * 1000; HEARTBEAT_INTERVAL = 5 * 1000; MAX_COMMAND_QUEUE_TIMEOUT = 120 * 1000; AUDIO_CODEC_ANALYZE_TIMEOUT = 650; KEEPALIVE_INTERVAL = 2 * 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; LOOP_RUNAWAY_LIMIT = 1000; /* * 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; p2pTurnHandshaking = {}; p2pTurnConfirmed = 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; localLookupRetryTimeout; lookupRetryTimeout; lookup2Timeout; lookup2RetryTimeout; 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; listeningPort = 0; dskKey = ""; dskExpiration = null; deviceSNs = {}; api; rawStation; customDataStaging = {}; lockPublicKey; lockAESKeys = new Map(); channel = http_1.Station.CHANNEL; encryption = types_1.EncryptionType.NONE; p2pKey; enableEmbeddedPKCS1Support = false; constructor(rawStation, api, ipAddress, listeningPort = 0, publicKey = "", enableEmbeddedPKCS1Support = false) { super(); this.api = api; this.lockPublicKey = publicKey; this.preferredIPAddress = ipAddress; if (listeningPort >= 0) this.listeningPort = listeningPort; this.enableEmbeddedPKCS1Support = enableEmbeddedPKCS1Support; this.cloudAddresses = (0, utils_1.decodeP2PCloudIPs)(rawStation.app_conn); logging_1.rootP2PLogger.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.p2pTurnHandshaking = {}; this.p2pTurnConfirmed = 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.connectAddress = undefined; this.customDataStaging = {}; 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)(this.enableEmbeddedPKCS1Support); 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; } _clearLocalLookupRetryTimeout() { this._clearTimeout(this.localLookupRetryTimeout); this.localLookupRetryTimeout = undefined; } _clearLookupRetryTimeout() { this._clearTimeout(this.lookupRetryTimeout); this.lookupRetryTimeout = undefined; } _clearLookup2RetryTimeout() { this._clearTimeout(this.lookup2RetryTimeout); this.lookup2RetryTimeout = 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); logging_1.rootP2PLogger.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._clearLocalLookupRetryTimeout(); 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) { logging_1.rootP2PLogger.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); logging_1.rootP2PLogger.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())) { logging_1.rootP2PLogger.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) { logging_1.rootP2PLogger.debug(`Trying to local lookup address for station ${this.rawStation.station_sn} with host ${host}`); this.localLookupByAddress({ host: host, port: 32108 }); this._clearLocalLookupRetryTimeout(); this.lookupRetryTimeout = setTimeout(() => { this.localLookup(host); }, this.LOCAL_LOOKUP_RETRY_TIMEOUT); } cloudLookup() { this.cloudAddresses.map((address) => this.cloudLookupByAddress(address)); this._clearLookupRetryTimeout(); this.lookupRetryTimeout = setTimeout(() => { this.cloudLookup(); }, this.LOOKUP_RETRY_TIMEOUT); } cloudLookup2() { this.cloudAddresses.map((address) => this.cloudLookupByAddress2(address)); this._clearLookup2RetryTimeout(); this.lookup2RetryTimeout = setTimeout(() => { this.cloudLookup2(); }, this.LOOKUP2_RETRY_TIMEOUT); } cloudLookupWithTurnServer(origAddress, data) { this.cloudAddresses.map((address) => this.cloudLookupByAddressWithTurnServer(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 cloudLookupByAddressWithTurnServer(address, origAddress, data) { // Send lookup message3 const msgId = types_1.RequestMessageType.TURN_LOOKUP_WITH_KEY; const payload = (0, utils_1.buildLookupWithKeyPayload3)(this.rawStation.p2p_did, origAddress, data); await this.sendMessage(`Cloud lookup addresses with turn server`, address, msgId, payload); } isConnected() { return this.connected; } _startConnectTimeout() { if (this.connectTimeout === undefined) this.connectTimeout = setTimeout(() => { logging_1.rootP2PLogger.warn(`Tried all hosts, no connection could be established to station ${this.rawStation.station_sn}.`); this._disconnected(); }, this.MAX_CONNECTION_TIMEOUT); } _connect(address, p2p_did) { logging_1.rootP2PLogger.debug(`Connecting to host ${address.host} on port ${address.port} (CHECK_CAM)`, { stationSN: this.rawStation.station_sn, address: address, p2pDid: p2p_did }); this.sendCamCheck(address, p2p_did); for (let i = address.port - 3; i < address.port; i++) this.sendCamCheck({ host: address.host, port: i }, p2p_did); for (let i = address.port + 1; i <= address.port + 3; i++) this.sendCamCheck({ host: address.host, port: i }, 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._clearLookup2Timeout(); this.lookup2Timeout = setTimeout(() => { this.cloudLookup2(); }, this.LOOKUP2_TIMEOUT); this._clearLookupTimeout(); this.lookupTimeout = setTimeout(() => { this.lookupTimeout = undefined; logging_1.rootP2PLogger.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(this.listeningPort, () => { this.binded = true; try { this.socket.setRecvBufferSize(this.UDP_RECVBUFFERSIZE_BYTES); this.socket.setBroadcast(true); } catch (err) { const error = (0, error_1.ensureError)(err); logging_1.rootP2PLogger.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) logging_1.rootP2PLogger.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; let nested_commandType2 = undefined; logging_1.rootP2PLogger.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; if (json.payload && json.payload.apiCommand !== undefined) { nested_commandType2 = json.payload.apiCommand; } } catch (err) { const error = (0, error_1.ensureError)(err); logging_1.rootP2PLogger.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); logging_1.rootP2PLogger.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, nested_commandType2); } 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.sendQueue.filter((queue) => queue.p2pCommand.commandType !== types_1.CommandType.CMD_PING && queue.p2pCommand.commandType !== types_1.CommandType.CMD_GET_DEVICE_PING).length > 0) { logging_1.rootP2PLogger.debug(`Initiate station p2p connection to send queued data`, { stationSN: this.rawStation.station_sn, queuedDataCount: this.sendQueue.filter((queue) => queue.p2pCommand.commandType !== types_1.CommandType.CMD_PING && queue.p2pCommand.commandType !== types_1.CommandType.CMD_GET_DEVICE_PING).length }); this.connect(); } } this.closeEnergySavingDevice(); } sendCommand(p2pcommand, p2pcommandType, nestedCommandType, customData, nested_commandType2) { const message = { p2pCommand: p2pcommand, nestedCommandType: nestedCommandType, nestedCommandType2: nested_commandType2, 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) { this._clearTimeout(messageState.retryTimeout); 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); logging_1.rootP2PLogger.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, nestedCommandType2: message.nestedCommandType2, channel: channel, data: data, retries: 0, acknowledged: false, 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 { logging_1.rootP2PLogger.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 { logging_1.rootP2PLogger.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: types_1.ErrorCode.ERROR_COMMAND_TIMEOUT, customData: message.customData }); if (message.commandType === types_1.CommandType.CMD_START_REALTIME_MEDIA || (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 === http_1.ParamType.COMMAND_START_LIVESTREAM && 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); } } 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.messageStates.delete(message.sequence); this.sendQueuedMessage(); return; } } const messageState = message; 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) { this.p2pSeqMapping.set(this.p2pDataSeqNumber, message.sequence); logging_1.rootP2PLogger.trace(`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); } logging_1.rootP2PLogger.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 === 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 === http_1.ParamType.COMMAND_START_LIVESTREAM && 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; this.waitForStreamData(types_1.P2PDataType.VIDEO); } 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; this.waitForStreamData(types_1.P2PDataType.BINARY); } 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._clearLocalLookupRetryTimeout(); 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, "")}`; logging_1.rootP2PLogger.trace(`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) { logging_1.rootP2PLogger.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 { logging_1.rootP2PLogger.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]}`; logging_1.rootP2PLogger.trace(`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") { logging_1.rootP2PLogger.trace(`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(); logging_1.rootP2PLogger.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(); logging_1.rootP2PLogger.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.TURN_SERVER_CAM_ID)) { // Answer from the device to a CAM_CHECK message if (!this.connected) { logging_1.rootP2PLogger.debug(`Received message - CAM_ID - Connected to station ${this.rawStation.station_sn} on host ${rinfo.address} port ${rinfo.port}`); this._clearLocalLookupRetryTimeout(); this._clearLookupRetryTimeout(); this._clearLookup2RetryTimeout(); 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.scheduleHeartbeat(); if (device_1.Device.isSmartSafe(this.rawStation.device_type)) { const payload = (0, utils_1.buildVoidCommandPayload)(http_1.Station.CHANNEL); 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: http_1.Station.CHANNEL, data: data, retries: 0, acknowledged: false, }; 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(http_1.Station.CHANNEL); 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)(http_1.Station.CHANNEL); 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: http_1.Station.CHANNEL, data: data, retries: 0, acknowledged: false, }; 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 v12 command to station`, this.connectAddress, types_1.RequestMessageType.DATA, data); tmpSendQueue.forEach(element => { this.sendQueue.push(element); }); } else if (this.rawStation.devices !== undefined && this.rawStation.devices !== null && this.rawStation.devices.length !== undefined && this.rawStation.devices.length > 0 && device_1.Device.isLockWifiVideo(this.rawStation.devices[0]?.device_type)) { const tmpSendQueue = [...this.sendQueue]; this.sendQueue = []; const payload = (0, utils_1.buildVoidCommandPayload)(http_1.Station.CHANNEL); 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: http_1.Station.CHANNEL, data: data, retries: 0, acknowledged: false, }; 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); 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(); if (this.energySavingDevice) { this.scheduleP2PKeepalive(); } 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 logging_1.rootP2PLogger.debug(`Received message - END`, { stationSN: this.rawStation.station_sn, remoteAddress: