UNPKG

zklib-ts

Version:

Unofficial zkteco library allows Node.js developers to easily interface with ZK BioMetric Fingerprint Attendance Devices

801 lines (798 loc) 34.4 kB
import { Socket } from 'net'; import { COMMANDS, Constants, REQUEST_DATA, DISCOVERED_CMD } from './helper/command'; import { ZkError } from './exceptions/handler'; import { authKey, checkNotEventTCP, decodeTCPHeader, createTCPHeader, removeTcpHeader, exportErrorMessage, decodeRTEvent, splitTcpPackets } from './helper/utils'; import { UserService } from './services/user.service'; import { TransactionService } from './services/transaction.service'; import { OptionsService } from './services/options.service'; class ZTCP { /** * @param_ip ip address of device * @param_port port number of device * @param_timeout connection timout * @param_comm_key communication key of device (if the case) * @return Zkteco TCP socket connection instance */ ip; port; timeout; sessionId = 0; replyId = 0; socket; comm_key; user_count = 0; fp_count = 0; pwd_count = 0; oplog_count = 0; attlog_count = 0; fp_cap = 0; user_cap = 0; attlog_cap = 0; fp_av = 0; user_av = 0; attlog_av = 0; face_count = 0; face_cap = 0; userPacketSize = 72; verbose = false; packetNumber = 0; replyData = Buffer.from([]); _optionsService; _transactionService; _userService; constructor(ip, port, timeout, comm_key, verbose) { this.ip = ip; this.port = port; this.timeout = timeout ? timeout : 10000; this.replyId = 0; this.comm_key = comm_key; this.verbose = verbose; this._optionsService = new OptionsService(this); this._userService = new UserService(this); this._transactionService = new TransactionService(this); } createSocket(cbError, cbClose) { return new Promise((resolve, reject) => { this.socket = new Socket(); // Handle socket error this.socket.once("error", (err) => { this.socket = undefined; // Ensure socket reference is cleared reject(err); if (typeof cbError === "function") cbError(err); }); // Handle successful connection this.socket.once("connect", () => { resolve(this.socket); }); // Handle socket closure this.socket.once("close", () => { this.socket = undefined; // Ensure socket reference is cleared if (typeof cbClose === "function") cbClose("tcp"); }); // Set socket timeout if provided if (this.timeout) { this.socket.setTimeout(this.timeout); } // Initiate connection this.socket.connect(this.port, this.ip); }); } async connect() { try { let reply = await this.executeCmd(COMMANDS.CMD_CONNECT, ""); if (reply.readUInt16LE(0) === COMMANDS.CMD_ACK_OK) { return true; } if (reply.readUInt16LE(0) === COMMANDS.CMD_ACK_UNAUTH) { const hashedCommkey = authKey(this.comm_key, this.sessionId); reply = await this.executeCmd(COMMANDS.CMD_AUTH, hashedCommkey); if (reply.readUInt16LE(0) === COMMANDS.CMD_ACK_OK) { return true; } else { throw new Error("error de authenticacion"); } } else { // No reply received; throw an error throw new Error("NO_REPLY_ON_CMD_CONNECT"); } } catch (err) { // Log the error for debugging, if necessary console.error("Failed to connect:", err); // Re-throw the error for handling by the caller throw err; } } async closeSocket() { return new Promise((resolve, reject) => { // If no socket is present, resolve immediately if (!this.socket) { return resolve(true); } // Clean up listeners to avoid potential memory leaks or duplicate handling this.socket.removeAllListeners("data"); // Set a timeout to handle cases where socket.end might not resolve const timer = setTimeout(() => { this.socket?.destroy(); // Forcibly close the socket if not closed properly resolve(true); // Resolve even if the socket was not closed properly }, 2000); // Close the socket and clear the timeout upon successful completion this.socket.end(() => { clearTimeout(timer); resolve(true); // Resolve once the socket has ended }); // Handle socket errors during closing this.socket.once("error", (err) => { clearTimeout(timer); reject(err); // Reject the promise with the error }); }); } writeMessage(msg, connect, cb) { return new Promise((resolve, reject) => { // Check if the socket is initialized if (!this.socket) { return reject(new Error("Socket is not initialized")); } // Define a variable for the timeout reference const timer = setTimeout(() => { // Check if the socket is still valid before trying to remove the listener cleanUp(); reject(new Error("TIMEOUT_ON_WRITING_MESSAGE")); // Reject the promise on timeout }, connect ? 3000 : this.timeout); // Handle incoming data const onData = (data) => { // Check if the socket is still valid before trying to remove the listener cleanUp(); // Clear the timeout once data is received resolve(cb(data)); // Resolve the promise with the received data }; const cleanUp = () => { if (timer) clearTimeout(timer); if (this.socket) { this.socket.removeListener("data", onData); // Remove the data event listener } }; // Attach the data event listener this.socket.on("data", onData); // Attempt to write the message to the socket this.socket.write(msg, undefined, (err) => { if (err) { cleanUp(); reject(err); // Reject the promise with the write error } }); }); } async requestData(msg) { try { return await new Promise((resolve, reject) => { let timer = null; let replyBuffer = Buffer.from([]); // Internal callback to handle data reception const internalCallback = (data_1) => { if (this.socket) { this.socket.removeListener("data", handleOnData); // Clean up listener } if (timer) clearTimeout(timer); // Clear the timeout resolve(data_1); // Resolve the promise with the data }; const onTimeOut = () => setTimeout(() => { if (this.socket) { this.socket.removeListener("data", handleOnData); // Clean up listener on timeout } reject(new Error("TIMEOUT_IN_RECEIVING_RESPONSE_AFTER_REQUESTING_DATA")); // Reject on timeout }, this.timeout); // Handle incoming data const handleOnData = (data_3) => { replyBuffer = Buffer.concat([replyBuffer, data_3]); // Accumulate data // Check if the data is a valid TCP event if (checkNotEventTCP(data_3)) return; // Decode the TCP header const header = decodeTCPHeader(replyBuffer.subarray(0, 16)); if (this.verbose) { console.log("response command: ", header.commandId, COMMANDS[header.commandId], "replyid: ", header.replyId); } // Handle based on command ID if (header.commandId === COMMANDS.CMD_DATA) { // Set a timeout to handle delayed responses timer = setTimeout(() => { internalCallback(replyBuffer); // Resolve with accumulated buffer }, 1000); } else { // Set a timeout to handle errors timer = onTimeOut(); // Extract packet length and handle accordingly const packetLength = data_3.readUIntLE(4, 2); if (packetLength > 8) { internalCallback(data_3); // Resolve immediately if sufficient data } } }; // Ensure the socket is valid before attaching the listener if (this.socket) { this.socket.on("data", handleOnData); // Write the message to the socket this.socket.write(msg, undefined, (err) => { if (err) { if (this.socket) { this.socket.removeListener("data", handleOnData); // Clean up listener on error } return reject(err); // Reject the promise with the error } // Set a timeout to handle cases where no response is received timer = onTimeOut(); }); } else { reject(new Error("SOCKET_NOT_INITIALIZED")); // Reject if socket is not initialized } }); } catch (err_1) { console.error("Promise Rejected:", err_1); // Log the rejection reason throw err_1; // Re-throw the error to be handled by the caller } } /** * * @param {*} command * @param {*} data * * * reject error when command fail and resolve data when success */ async executeCmd(command, data) { // Reset sessionId and replyId for connection commands if (command === COMMANDS.CMD_CONNECT) { this.sessionId = 0; this.replyId = 0; } else { this.replyId++; } const currentReply = this.replyId; const buf = createTCPHeader(command, this.sessionId, this.replyId, data); const callback = (responseData) => { const packets = splitTcpPackets(responseData); for (const packet of packets) { const headers = decodeTCPHeader(packet); if (this.verbose) { const JOIN_CMD = { ...COMMANDS, ...DISCOVERED_CMD }; console.debug("request command:", COMMANDS[command], "\nresponse command: ", JOIN_CMD[headers.commandId], "replyid: ", headers.replyId); } if (+headers.replyId === currentReply + 1) { return packet; } continue; } }; return new Promise((Resolve, Reject) => { // Write the message to the socket and wait for a response this.writeMessage(buf, command === COMMANDS.CMD_CONNECT || command === COMMANDS.CMD_EXIT, callback) .then((reply) => { // Remove TCP header from the response let rReply; try { rReply = removeTcpHeader(reply); } catch (e) { console.log("reply", reply); } // Update sessionId for connection command responses if (command === COMMANDS.CMD_CONNECT && rReply && rReply.length >= 6) { // Assuming sessionId is located at offset 4 and is 2 bytes long this.sessionId = rReply.readUInt16LE(4); } Resolve(rReply); }) .catch((err) => { // Log or handle the error if necessary console.error("Error executing command:", err); Reject(err); // Re-throw the error for handling by the caller }); }); } async sendChunkRequest(start, size) { this.replyId++; const reqData = Buffer.alloc(8); reqData.writeUInt32LE(start, 0); reqData.writeUInt32LE(size, 4); const buf = createTCPHeader(COMMANDS.CMD_DATA_RDY, this.sessionId, this.replyId, reqData); try { const promise = new Promise((resolve, reject) => { this.socket?.write(buf, undefined, (err) => { if (err) { console.error(`[TCP][SEND_CHUNK_REQUEST] Error sending chunk request: ${err.message}`); reject(err); // Reject the promise if there is an error } else { resolve(true); // Resolve the promise if the write operation succeeds } }); }); await promise; } catch (err) { // Handle or log the error as needed console.error(`[TCP][SEND_CHUNK_REQUEST] Exception: ${err.message}`); throw err; // Re-throw the error for handling by the caller } } /** * * @param {Buffer} reqData - indicate the type of data that need to receive ( user or attLog) * @param {Function} cb - callback is triggered when receiving packets * * readWithBuffer will reject error if it'wrong when starting request data * readWithBuffer will return { data: replyData , err: Error } when receiving requested data */ readWithBuffer(reqData, cb) { return new Promise(async (resolve, reject) => { this.replyId++; const buf = createTCPHeader(COMMANDS.CMD_DATA_WRRQ, this.sessionId, this.replyId, reqData); let reply; try { reply = await this.requestData(buf); } catch (err) { reject(err); } const header = decodeTCPHeader(reply?.subarray(0, 16)); switch (header.commandId) { case COMMANDS.CMD_DATA: { resolve({ data: reply.subarray(16), mode: 8 }); break; } case COMMANDS.CMD_ACK_OK: case COMMANDS.CMD_PREPARE_DATA: { // this case show that data is prepared => send command to get these data // reply variable includes information about the size of following data const recvData = reply.subarray(16); const size = recvData.readUIntLE(1, 4); // We need to split the data to many chunks to receive , because it's to large // After receiving all chunk data , we concat it to TotalBuffer variable , that 's the data we want const remain = size % Constants.MAX_CHUNK; const numberChunks = Math.round(size - remain) / Constants.MAX_CHUNK; this.packetNumber = numberChunks + (remain > 0 ? 1 : 0); //let replyData = Buffer.from([]) let totalBuffer = Buffer.from([]); let realTotalBuffer = Buffer.from([]); let timer = setTimeout(() => { internalCallback(this.replyData, new Error("TIMEOUT WHEN RECEIVING PACKET")); }, this.timeout); const internalCallback = (replyData, err = null) => { this.socket && this.socket.removeAllListeners("data"); timer && clearTimeout(timer); resolve({ data: replyData, err }); }; this.socket?.once("close", () => { internalCallback(this.replyData, new Error("Socket is disconnected unexpectedly")); }); for (let i = 0; i <= numberChunks; i++) { const data = await new Promise((resolve2, reject2) => { try { this.sendChunkRequest(i * Constants.MAX_CHUNK, i === numberChunks ? remain : Constants.MAX_CHUNK); this.socket?.on("data", (reply) => { clearTimeout(timer); timer = setTimeout(() => { internalCallback(this.replyData, new Error(`TIME OUT !! ${this.packetNumber} PACKETS REMAIN !`)); }, this.timeout); if (this.verbose && reply.length >= 8) { const headers = decodeTCPHeader(reply); if (COMMANDS[headers.commandId]) { switch (headers.commandId) { case COMMANDS.CMD_ACK_OK: case COMMANDS.CMD_DATA: this.verbose && console.log("CMD received: ", COMMANDS[headers.commandId]); break; case COMMANDS.CMD_PREPARE_DATA: this.verbose && console.log("CMD received: ", COMMANDS[headers.commandId]); this.verbose && console.log(`recieve chunk: prepare data size is ${headers.payloadSize}`); break; default: break; } } } totalBuffer = Buffer.concat([totalBuffer, reply]); const packetLength = totalBuffer.readUIntLE(4, 2); if (totalBuffer.length >= 8 + packetLength) { realTotalBuffer = Buffer.concat([ realTotalBuffer, totalBuffer.subarray(16, 8 + packetLength), ]); totalBuffer = totalBuffer.subarray(8 + packetLength); if ((this.packetNumber > 1 && realTotalBuffer.length === Constants.MAX_CHUNK + 8) || (this.packetNumber === 1 && realTotalBuffer.length === remain + 8)) { this.packetNumber--; cb && cb(realTotalBuffer.length, size); resolve2(realTotalBuffer.subarray(8)); totalBuffer = Buffer.from([]); realTotalBuffer = Buffer.from([]); } } }); } catch (e) { reject2(e); } }); this.replyData = Buffer.concat([ this.replyData, data, ]); this.socket?.removeAllListeners("data"); if (this.packetNumber <= 0) { resolve({ data: this.replyData }); } } break; } default: { reject(new Error("ERROR_IN_UNHANDLE_CMD " + exportErrorMessage(header.commandId))); } } }); } /** * * @param {*} callbackInProcess * reject error when starting request data * return { data: records, err: Error } when receiving requested data */ async freeData() { try { const resp = await this.executeCmd(COMMANDS.CMD_FREE_DATA, ""); return !!resp; } catch (err) { console.error("Error freeing data:", err); throw err; // Optionally, re-throw the error if you need to handle it upstream } } async disableDevice() { try { const resp = await this.executeCmd(COMMANDS.CMD_DISABLEDEVICE, REQUEST_DATA.DISABLE_DEVICE); return !!resp; } catch (err) { console.error("Error disabling device:", err); throw err; // Optionally, re-throw the error if you need to handle it upstream } } async enableDevice() { try { const resp = await this.executeCmd(COMMANDS.CMD_ENABLEDEVICE, ""); return !!resp; } catch (err) { console.error("Error enabling device:", err); throw err; // Optionally, re-throw the error if you need to handle it upstream } } async disconnect() { try { // Attempt to execute the disconnect command await this.executeCmd(COMMANDS.CMD_EXIT, ""); } catch (err) { // Log any errors encountered during command execution console.error("Error during disconnection:", err); // Optionally, add more handling or recovery logic here } // Attempt to close the socket and return the result try { await this.closeSocket(); } catch (err) { // Log any errors encountered while closing the socket console.error("Error during socket closure:", err); // Optionally, rethrow or handle the error if necessary throw err; // Re-throwing to propagate the error } } async getInfo() { try { // Execute the command to retrieve free sizes from the device const data = await this.executeCmd(COMMANDS.CMD_GET_FREE_SIZES, ""); // Parse the response data to extract and return relevant information return { userCounts: data.readUIntLE(24, 4), // Number of users logCounts: data.readUIntLE(40, 4), // Number of logs logCapacity: data.readUIntLE(72, 4), // Capacity of logs in bytes }; } catch (err) { // Log the error for debugging purposes console.error("Error getting device info:", err); // Re-throw the error to allow upstream error handling throw err; } } async getSizes() { try { // Execute the command to retrieve free sizes from the device const data = await this.executeCmd(COMMANDS.CMD_GET_FREE_SIZES, ""); // Parse the response data to extract and return relevant information const buf = data.slice(8); // remove header this.user_count = buf.readUIntLE(16, 4); this.fp_count = buf.readUIntLE(24, 4); this.pwd_count = buf.readUIntLE(52, 4); this.oplog_count = buf.readUIntLE(40, 4); this.attlog_count = buf.readUIntLE(32, 4); this.fp_cap = buf.readUIntLE(56, 4); this.user_cap = buf.readUIntLE(60, 4); this.attlog_cap = buf.readUIntLE(64, 4); this.fp_av = buf.readUIntLE(68, 4); this.user_av = buf.readUIntLE(72, 4); this.attlog_av = buf.readUIntLE(76, 4); this.face_count = buf.readUIntLE(80, 4); this.face_cap = buf.readUIntLE(88, 4); return { userCounts: this.user_count, // Number of users logCounts: this.attlog_count, // Number of logs fingerCount: this.fp_count, adminCount: this.pwd_count, opLogCount: this.oplog_count, logCapacity: this.attlog_cap, // Capacity of logs in bytes fingerCapacity: this.fp_cap, userCapacity: this.user_cap, attLogCapacity: this.attlog_cap, fingerAvailable: this.fp_av, userAvailable: this.user_av, attLogAvailable: this.attlog_av, faceCount: this.face_count, faceCapacity: this.face_cap, }; } catch (err) { // Log the error for debugging purposes console.error("Error getting device info:", err); // Re-throw the error to allow upstream error handling throw err; } } #listeners = new Map(); async getAttendanceSize() { try { // Execute command to get free sizes const data = await this.executeCmd(COMMANDS.CMD_GET_FREE_SIZES, ""); // Parse and return the attendance size return data.readUIntLE(40, 4); // Assuming data at offset 40 represents the attendance size } catch (err) { // Log error details for debugging console.error("Error getting attendance size:", err); // Re-throw the error to be handled by the caller throw err; } } // Clears the attendance logs on the device async clearAttendanceLog() { return await this._transactionService.clearAttendanceLog(); } /** * Clears all data on the device * @value 1 Attendance records * @value 2 Fingerprint templates * @value 3 None * @value 4 Operation records * @value 5 User information * @default 0 Delete all */ async clearData(value) { try { // Execute the command to clear all data await this.disableDevice(); if (!value) value = 3; const buf = await this.executeCmd(COMMANDS.CMD_CLEAR_DATA, value.toString()); await this.refreshData(); await this.enableDevice(); return !!buf; } catch (err) { // Log the error for debugging purposes console.error("Error clearing data:", err); // Re-throw the error to be handled by the caller throw err; } } async getRealTimeLogs(cb = (realTimeLog) => { }) { this.replyId++; // Increment the reply ID for this request try { // Create a buffer with the command header to request real-time logs const buf = createTCPHeader(COMMANDS.CMD_REG_EVENT, this.sessionId, this.replyId, Buffer.from([0x01, 0x00, 0x00, 0x00])); // Send the request to the device this.socket?.write(buf, undefined, (err) => { if (err) { // Log and reject the promise if there is an error writing to the socket console.error("Error sending real-time logs request:", err); throw err; } }); // Ensure data listeners are added only once if (this.socket?.listenerCount("data") === 0) { console.log("entraaa"); this.socket.on("data", (data) => { // Check if the data is an event and not just a regular response if (checkNotEventTCP(data)) { // Process the data if it is of the expected length if (data.length > 16) { // Decode and pass the log to the callback cb(decodeRTEvent(data)); } } }); } } catch (err) { // Handle errors and reject the promise console.error("Error getting real-time logs:", err); throw err; } } /** * Get all Finger objects * @returns {Record<string, Finger[]>} */ async getTemplates(callbackInProcess = () => { }) { return await this._userService.getTemplates(callbackInProcess); } /** * Return size * @param packet */ testTcpTop(packet) { // Check if packet is too small if (packet.length <= 8) return 0; // Extract header values using little-endian format const headerValue1 = packet.readUInt16LE(0); const headerValue2 = packet.readUInt16LE(2); const size = packet.readUInt32LE(4); // Check if magic numbers match if (headerValue1 === Constants.MACHINE_PREPARE_DATA_1 && headerValue2 === Constants.MACHINE_PREPARE_DATA_2) { return size; } return 0; } async refreshData() { try { const reply = await this.executeCmd(COMMANDS.CMD_REFRESHDATA, ""); return !!reply; } catch (err) { console.error("Error getting user templates: ", err); throw err; } } async sendWithBuffer(buffer) { const MAX_CHUNK = 1024; const size = buffer.length; await this.freeData(); const commandString = Buffer.alloc(4); // 'I' is 4 bytes commandString.writeUInt32LE(size, 0); try { const cmdResponse = await this.executeCmd(COMMANDS.CMD_PREPARE_DATA, commandString); // responds with 2000 = CMD_ACK_OK if (!cmdResponse) { throw new Error("Can't prepare data"); } } catch (e) { console.error(e); } const remain = size % MAX_CHUNK; const packets = Math.floor((size - remain) / MAX_CHUNK); let start = 0; try { for (let i = 0; i < packets; i++) { const resp = await this.sendChunk(buffer.slice(start, start + MAX_CHUNK)); if (resp) { start += MAX_CHUNK; if (i == packets - 1 && remain) { const lastPacket = await this.sendChunk(buffer.slice(start, start + remain)); return lastPacket; } } } } catch (e) { console.error(e); } } async sendChunk(commandString) { try { return await new Promise((resolve, reject) => { resolve(this.executeCmd(COMMANDS.CMD_DATA, commandString)); }); } catch (e) { throw new ZkError(e, COMMANDS.CMD_DATA, this.ip); } } async readSocket(length, cb = null) { let replyBufer = Buffer.from([]); const totalPackets = 0; return new Promise((resolve, reject) => { let timer = setTimeout(() => { internalCallback(replyBufer, new Error("TIMEOUT WHEN RECEIVING PACKET")); }, this.timeout); const internalCallback = (replyData, err = null) => { this.socket && this.socket.removeListener("data", onDataEnroll); timer && clearTimeout(timer); resolve({ data: replyData, err: err }); }; function onDataEnroll(data) { clearTimeout(timer); timer = setTimeout(() => { internalCallback(replyBufer, new Error(`TIME OUT !! ${totalPackets} PACKETS REMAIN !`)); }, this.timeout); replyBufer = Buffer.concat([replyBufer, data], replyBufer.length + data.length); if (data.length == length) { internalCallback(data); } } this.socket.once("close", () => { internalCallback(replyBufer, new Error("Socket is disconnected unexpectedly")); }); this.socket.on("data", onDataEnroll); }).catch((err) => { console.error("Promise Rejected:", err); // Log the rejection reason throw err; // Re-throw the error to be handled by the caller }); } /** * Register events * @param {number} flags - Event flags * @returns {Promise<void>} * @throws {ZKErrorResponse} If registration fails */ async regEvent(flags) { try { const commandString = Buffer.alloc(4); // 'I' format is 4 bytes commandString.writeUInt32LE(flags, 0); // Little-endian unsigned int const cmdResponse = await this.executeCmd(COMMANDS.CMD_REG_EVENT, commandString); if (this.verbose) console.log("regEvent: ", cmdResponse.readUInt16LE(0)); } catch (e) { throw new ZkError(e, COMMANDS.CMD_REG_EVENT, this.ip); } } async cancelCapture() { try { const reply = await this.executeCmd(COMMANDS.CMD_CANCELCAPTURE, ""); return !!reply; } catch (e) { throw new ZkError(e, COMMANDS.CMD_CANCELCAPTURE, this.ip); } } async restartDevice() { try { await this.executeCmd(COMMANDS.CMD_RESTART, ""); } catch (e) { throw new ZkError(e, COMMANDS.CMD_RESTART, this.ip); } } } export { ZTCP };