UNPKG

zklib-ts

Version:

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

1,217 lines (1,207 loc) 111 kB
import { Socket } from 'net'; import { appendFile } from 'fs'; import * as dgram from 'node:dgram'; var COMMANDS; (function (COMMANDS) { COMMANDS[COMMANDS["CMD_ACK_DATA"] = 2002] = "CMD_ACK_DATA"; COMMANDS[COMMANDS["CMD_ACK_ERROR"] = 2001] = "CMD_ACK_ERROR"; COMMANDS[COMMANDS["CMD_ACK_ERROR_CMD"] = 65533] = "CMD_ACK_ERROR_CMD"; COMMANDS[COMMANDS["CMD_ACK_ERROR_DATA"] = 65531] = "CMD_ACK_ERROR_DATA"; COMMANDS[COMMANDS["CMD_ACK_ERROR_INIT"] = 65532] = "CMD_ACK_ERROR_INIT"; COMMANDS[COMMANDS["CMD_ACK_OK"] = 2000] = "CMD_ACK_OK"; COMMANDS[COMMANDS["CMD_ACK_REPEAT"] = 2004] = "CMD_ACK_REPEAT"; COMMANDS[COMMANDS["CMD_ACK_RETRY"] = 2003] = "CMD_ACK_RETRY"; COMMANDS[COMMANDS["CMD_ACK_UNAUTH"] = 2005] = "CMD_ACK_UNAUTH"; COMMANDS[COMMANDS["CMD_ACK_UNKNOWN"] = 65535] = "CMD_ACK_UNKNOWN"; COMMANDS[COMMANDS["CMD_ATTLOG_RRQ"] = 13] = "CMD_ATTLOG_RRQ"; COMMANDS[COMMANDS["CMD_AUTH"] = 1102] = "CMD_AUTH"; COMMANDS[COMMANDS["CMD_CANCELCAPTURE"] = 62] = "CMD_CANCELCAPTURE"; COMMANDS[COMMANDS["CMD_CAPTUREFINGER"] = 1009] = "CMD_CAPTUREFINGER"; COMMANDS[COMMANDS["CMD_CAPTUREIMAGE"] = 1012] = "CMD_CAPTUREIMAGE"; COMMANDS[COMMANDS["CMD_CHANGE_SPEED"] = 1101] = "CMD_CHANGE_SPEED"; COMMANDS[COMMANDS["CMD_CHECKSUM_BUFFER"] = 119] = "CMD_CHECKSUM_BUFFER"; COMMANDS[COMMANDS["CMD_CLEAR_ACC"] = 32] = "CMD_CLEAR_ACC"; COMMANDS[COMMANDS["CMD_CLEAR_ADMIN"] = 20] = "CMD_CLEAR_ADMIN"; COMMANDS[COMMANDS["CMD_CLEAR_ATTLOG"] = 15] = "CMD_CLEAR_ATTLOG"; COMMANDS[COMMANDS["CMD_CLEAR_DATA"] = 14] = "CMD_CLEAR_DATA"; COMMANDS[COMMANDS["CMD_CLEAR_LCD"] = 67] = "CMD_CLEAR_LCD"; COMMANDS[COMMANDS["CMD_CLEAR_OPLOG"] = 33] = "CMD_CLEAR_OPLOG"; COMMANDS[COMMANDS["CMD_CONNECT"] = 1000] = "CMD_CONNECT"; COMMANDS[COMMANDS["CMD_DATA"] = 1501] = "CMD_DATA"; COMMANDS[COMMANDS["CMD_DATA_RDY"] = 1504] = "CMD_DATA_RDY"; COMMANDS[COMMANDS["CMD_DATA_WRRQ"] = 1503] = "CMD_DATA_WRRQ"; COMMANDS[COMMANDS["CMD_DB_RRQ"] = 7] = "CMD_DB_RRQ"; COMMANDS[COMMANDS["CMD_DEL_FPTMP"] = 134] = "CMD_DEL_FPTMP"; COMMANDS[COMMANDS["CMD_DELETE_SMS"] = 72] = "CMD_DELETE_SMS"; COMMANDS[COMMANDS["CMD_DELETE_UDATA"] = 74] = "CMD_DELETE_UDATA"; COMMANDS[COMMANDS["CMD_DELETE_USER"] = 18] = "CMD_DELETE_USER"; COMMANDS[COMMANDS["CMD_DELETE_USERTEMP"] = 19] = "CMD_DELETE_USERTEMP"; COMMANDS[COMMANDS["CMD_DISABLEDEVICE"] = 1003] = "CMD_DISABLEDEVICE"; COMMANDS[COMMANDS["CMD_DOORSTATE_RRQ"] = 75] = "CMD_DOORSTATE_RRQ"; COMMANDS[COMMANDS["CMD_EMPTY_MIFARE"] = 78] = "CMD_EMPTY_MIFARE"; COMMANDS[COMMANDS["CMD_ENABLE_CLOCK"] = 57] = "CMD_ENABLE_CLOCK"; COMMANDS[COMMANDS["CMD_ENABLEDEVICE"] = 1002] = "CMD_ENABLEDEVICE"; COMMANDS[COMMANDS["CMD_EXIT"] = 1001] = "CMD_EXIT"; COMMANDS[COMMANDS["CMD_FREE_DATA"] = 1502] = "CMD_FREE_DATA"; COMMANDS[COMMANDS["CMD_GET_FREE_SIZES"] = 50] = "CMD_GET_FREE_SIZES"; COMMANDS[COMMANDS["CMD_GET_PINWIDTH"] = 69] = "CMD_GET_PINWIDTH"; COMMANDS[COMMANDS["CMD_GET_TIME"] = 201] = "CMD_GET_TIME"; COMMANDS[COMMANDS["CMD_GET_USERTEMP"] = 88] = "CMD_GET_USERTEMP"; COMMANDS[COMMANDS["CMD_GET_VERSION"] = 1100] = "CMD_GET_VERSION"; COMMANDS[COMMANDS["CMD_GRPTZ_RRQ"] = 25] = "CMD_GRPTZ_RRQ"; COMMANDS[COMMANDS["CMD_GRPTZ_WRQ"] = 26] = "CMD_GRPTZ_WRQ"; COMMANDS[COMMANDS["CMD_OPLOG_RRQ"] = 34] = "CMD_OPLOG_RRQ"; COMMANDS[COMMANDS["CMD_OPTIONS_RRQ"] = 11] = "CMD_OPTIONS_RRQ"; COMMANDS[COMMANDS["CMD_OPTIONS_WRQ"] = 12] = "CMD_OPTIONS_WRQ"; COMMANDS[COMMANDS["CMD_POWEROFF"] = 1005] = "CMD_POWEROFF"; COMMANDS[COMMANDS["CMD_PREPARE_DATA"] = 1500] = "CMD_PREPARE_DATA"; COMMANDS[COMMANDS["CMD_REFRESHDATA"] = 1013] = "CMD_REFRESHDATA"; COMMANDS[COMMANDS["CMD_REFRESHOPTION"] = 1014] = "CMD_REFRESHOPTION"; COMMANDS[COMMANDS["CMD_REG_EVENT"] = 500] = "CMD_REG_EVENT"; COMMANDS[COMMANDS["CMD_RESTART"] = 1004] = "CMD_RESTART"; COMMANDS[COMMANDS["CMD_RESUME"] = 1007] = "CMD_RESUME"; COMMANDS[COMMANDS["CMD_SET_TIME"] = 202] = "CMD_SET_TIME"; COMMANDS[COMMANDS["CMD_SLEEP"] = 1006] = "CMD_SLEEP"; COMMANDS[COMMANDS["CMD_SMS_RRQ"] = 71] = "CMD_SMS_RRQ"; COMMANDS[COMMANDS["CMD_SMS_WRQ"] = 70] = "CMD_SMS_WRQ"; COMMANDS[COMMANDS["CMD_STARTENROLL"] = 61] = "CMD_STARTENROLL"; COMMANDS[COMMANDS["CMD_STARTVERIFY"] = 60] = "CMD_STARTVERIFY"; COMMANDS[COMMANDS["CMD_STATE_RRQ"] = 64] = "CMD_STATE_RRQ"; COMMANDS[COMMANDS["CMD_TEST_TEMP"] = 1011] = "CMD_TEST_TEMP"; COMMANDS[COMMANDS["CMD_TESTVOICE"] = 1017] = "CMD_TESTVOICE"; COMMANDS[COMMANDS["CMD_TMP_WRITE"] = 87] = "CMD_TMP_WRITE"; COMMANDS[COMMANDS["CMD_TZ_RRQ"] = 27] = "CMD_TZ_RRQ"; COMMANDS[COMMANDS["CMD_TZ_WRQ"] = 28] = "CMD_TZ_WRQ"; COMMANDS[COMMANDS["CMD_UDATA_WRQ"] = 73] = "CMD_UDATA_WRQ"; COMMANDS[COMMANDS["CMD_ULG_RRQ"] = 29] = "CMD_ULG_RRQ"; COMMANDS[COMMANDS["CMD_ULG_WRQ"] = 30] = "CMD_ULG_WRQ"; COMMANDS[COMMANDS["CMD_UNLOCK"] = 31] = "CMD_UNLOCK"; COMMANDS[COMMANDS["CMD_USER_WRQ"] = 8] = "CMD_USER_WRQ"; COMMANDS[COMMANDS["CMD_USERGRP_RRQ"] = 21] = "CMD_USERGRP_RRQ"; COMMANDS[COMMANDS["CMD_USERGRP_WRQ"] = 22] = "CMD_USERGRP_WRQ"; COMMANDS[COMMANDS["CMD_USERTEMP_RRQ"] = 9] = "CMD_USERTEMP_RRQ"; COMMANDS[COMMANDS["CMD_USERTEMP_WRQ"] = 10] = "CMD_USERTEMP_WRQ"; COMMANDS[COMMANDS["CMD_USERTZ_RRQ"] = 23] = "CMD_USERTZ_RRQ"; COMMANDS[COMMANDS["CMD_USERTZ_WRQ"] = 24] = "CMD_USERTZ_WRQ"; COMMANDS[COMMANDS["CMD_VERIFY_RRQ"] = 80] = "CMD_VERIFY_RRQ"; COMMANDS[COMMANDS["CMD_VERIFY_WRQ"] = 79] = "CMD_VERIFY_WRQ"; COMMANDS[COMMANDS["CMD_WRITE_LCD"] = 66] = "CMD_WRITE_LCD"; COMMANDS[COMMANDS["CMD_WRITE_MIFARE"] = 76] = "CMD_WRITE_MIFARE"; COMMANDS[COMMANDS["EF_ALARM"] = 512] = "EF_ALARM"; COMMANDS[COMMANDS["EF_ATTLOG"] = 1] = "EF_ATTLOG"; COMMANDS[COMMANDS["EF_BUTTON"] = 16] = "EF_BUTTON"; COMMANDS[COMMANDS["EF_ENROLLFINGER"] = 8] = "EF_ENROLLFINGER"; COMMANDS[COMMANDS["EF_ENROLLUSER"] = 4] = "EF_ENROLLUSER"; COMMANDS[COMMANDS["EF_FINGER"] = 2] = "EF_FINGER"; COMMANDS[COMMANDS["EF_FPFTR"] = 256] = "EF_FPFTR"; COMMANDS[COMMANDS["EF_UNLOCK"] = 32] = "EF_UNLOCK"; COMMANDS[COMMANDS["EF_VERIFY"] = 128] = "EF_VERIFY"; })(COMMANDS || (COMMANDS = {})); var Constants; (function (Constants) { Constants[Constants["USHRT_MAX"] = 65535] = "USHRT_MAX"; Constants[Constants["MAX_CHUNK"] = 65472] = "MAX_CHUNK"; Constants[Constants["MACHINE_PREPARE_DATA_1"] = 20560] = "MACHINE_PREPARE_DATA_1"; Constants[Constants["MACHINE_PREPARE_DATA_2"] = 32130] = "MACHINE_PREPARE_DATA_2"; })(Constants || (Constants = {})); const REQUEST_DATA = { DISABLE_DEVICE: Buffer.from([0, 0, 0, 0]), GET_REAL_TIME_EVENT: Buffer.from([0x01, 0x00, 0x00, 0x00]), GET_ATTENDANCE_LOGS: Buffer.from([0x01, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), GET_USERS: Buffer.from([0x01, 0x09, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), GET_TEMPLATES: Buffer.from([0x01, 0x07, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) }; /** * * @param {number} time */ const decode = time => { const second = time % 60; time = (time - second) / 60; const minute = time % 60; time = (time - minute) / 60; const hour = time % 24; time = (time - hour) / 24; const day = time % 31 + 1; time = (time - (day - 1)) / 31; const month = time % 12; time = (time - month) / 12; const year = time + 2000; return new Date(year, month, day, hour, minute, second); }; /** * * @param {Date} date */ const encode = date => { return (((date.getFullYear() % 100) * 12 * 31 + date.getMonth() * 31 + date.getDate() - 1) * (24 * 60 * 60) + (date.getHours() * 60 + date.getMinutes()) * 60 + date.getSeconds()); }; var timeParser = { encode, decode }; const parseCurrentTime = () => { const currentTime = new Date(); return { year: currentTime.getFullYear(), month: currentTime.getMonth() + 1, day: currentTime.getDate(), hour: currentTime.getHours(), second: currentTime.getSeconds() }; }; const log = (text) => { const currentTime = parseCurrentTime(); const fileName = `${currentTime.day}`.padStart(2, '0') + `${currentTime.month}`.padStart(2, '0') + `${currentTime.year}.err.log`; const logMessage = `\n [${currentTime.hour}:${currentTime.second}] ${text}`; appendFile(fileName, logMessage, () => { }); }; /** * Represents a User as is from ZkDevice and contain methods * */ class User { uid; name; privilege; password; group_id; user_id; card; /** * Creates a new User instance * @param uid User ID * @param name User name * @param privilege Privilege level * @param password User password (default: "") * @param group_id Group ID (default: "") * @param user_id Alternate user ID (default: "") * @param card Card number (default: 0) */ constructor(uid, name, privilege, password = "", group_id = "", user_id = "", card = 0) { this.uid = uid; this.name = name; this.privilege = privilege; this.password = password; this.group_id = group_id; this.user_id = user_id; this.card = card; } ensureEncoding(string) { try { return decodeURIComponent(string); } catch (e) { return unescape(string); } } repack29() { // Pack format: <BHB5s8sIxBhI (total 29 bytes) const buf = Buffer.alloc(29); let offset = 0; buf.writeUInt8(2, offset); offset += 1; buf.writeUInt16LE(this.uid, offset); offset += 2; buf.writeUInt8(this.privilege, offset); offset += 1; const passwordBuf = Buffer.from(this.ensureEncoding(this.password)); passwordBuf.copy(buf, offset, 0, 5); offset += 5; const nameBuf = Buffer.from(this.ensureEncoding(this.name)); nameBuf.copy(buf, offset, 0, 8); offset += 8; buf.writeUInt32LE(this.card, offset); offset += 4; offset += 1; // padding byte buf.writeUInt8(0, offset); offset += 1; buf.writeUInt32LE(parseInt(this.user_id) || 0, offset); return buf; } repack73() { // Pack format: <BHB8s24sIB7sx24s (total 73 bytes) const buf = Buffer.alloc(73); let offset = 0; buf.writeUInt8(2, offset); offset += 1; buf.writeUInt16LE(this.uid, offset); offset += 2; buf.writeUInt8(this.privilege, offset); offset += 1; const passwordBuf = Buffer.from(this.ensureEncoding(this.password)); passwordBuf.copy(buf, offset, 0, 8); offset += 8; const nameBuf = Buffer.from(this.ensureEncoding(this.name)); nameBuf.copy(buf, offset, 0, 24); offset += 24; buf.writeUInt32LE(this.card, offset); offset += 4; buf.writeUInt8(1, offset); offset += 1; const groupBuf = Buffer.from(this.ensureEncoding(String(this.group_id))); groupBuf.copy(buf, offset, 0, 7); offset += 8; const userIdBuf = Buffer.from(this.ensureEncoding(String(this.user_id))); userIdBuf.copy(buf, offset, 0, 24); return buf; } } class Attendance { sn; user_id; record_time; type; state; ip; constructor(sn, user_id, record_time, type, state) { this.sn = sn; this.user_id = user_id; this.record_time = record_time; this.type = type; this.state = state; } } const parseHexToTime = (hex) => { const time = { year: hex.readUIntLE(0, 1), month: hex.readUIntLE(1, 1), date: hex.readUIntLE(2, 1), hour: hex.readUIntLE(3, 1), minute: hex.readUIntLE(4, 1), second: hex.readUIntLE(5, 1) }; return new Date(2000 + time.year, time.month - 1, time.date, time.hour, time.minute, time.second); }; const createChkSum = (buf) => { let chksum = 0; for (let i = 0; i < buf.length; i += 2) { if (i === buf.length - 1) { chksum += buf[i]; } else { chksum += buf.readUInt16LE(i); } chksum %= Constants.USHRT_MAX; } chksum = Constants.USHRT_MAX - chksum - 1; return chksum; }; const createUDPHeader = (command, sessionId, replyId, data) => { const dataBuffer = Buffer.from(data); const buf = Buffer.alloc(8 + dataBuffer.length); buf.writeUInt16LE(command, 0); buf.writeUInt16LE(0, 2); buf.writeUInt16LE(sessionId, 4); buf.writeUInt16LE(replyId, 6); dataBuffer.copy(buf, 8); const chksum2 = createChkSum(buf); buf.writeUInt16LE(chksum2, 2); replyId = (replyId + 1) % Constants.USHRT_MAX; buf.writeUInt16LE(replyId, 6); return buf; }; const createTCPHeader = (command, sessionId, replyId, data) => { const dataBuffer = Buffer.from(data); const buf = Buffer.alloc(8 + dataBuffer.length); buf.writeUInt16LE(command, 0); buf.writeUInt16LE(0, 2); buf.writeUInt16LE(sessionId, 4); buf.writeUInt16LE(replyId, 6); dataBuffer.copy(buf, 8); const chksum2 = createChkSum(buf); buf.writeUInt16LE(chksum2, 2); replyId = (replyId + 1) % Constants.USHRT_MAX; buf.writeUInt16LE(replyId, 6); const prefixBuf = Buffer.from([0x50, 0x50, 0x82, 0x7d, 0x13, 0x00, 0x00, 0x00]); prefixBuf.writeUInt16LE(buf.length, 4); return Buffer.concat([prefixBuf, buf]); }; const removeTcpHeader = (buf) => { if (buf.length < 8) { return buf; } if (buf.compare(Buffer.from([0x50, 0x50, 0x82, 0x7d]), 0, 4, 0, 4) !== 0) { return buf; } return buf.slice(8); }; const parseTimeToDate = (time) => { const second = time % 60; time = (time - second) / 60; const minute = time % 60; time = (time - minute) / 60; const hour = time % 24; time = (time - hour) / 24; const day = time % 31 + 1; time = (time - (day - 1)) / 31; const month = time % 12; time = (time - month) / 12; const year = time + 2000; return new Date(Date.UTC(year, month, day, hour, minute, second)); }; const decodeUserData28 = (userData) => { return { uid: userData.readUIntLE(0, 2), privilege: userData.readUIntLE(2, 1), name: userData .slice(8, 8 + 8) .toString('ascii') .split('\0') .shift() || '', user_id: userData.readUIntLE(24, 4).toString(), }; }; const decodeUserData72 = (userData) => { return new User(userData.readUIntLE(0, 2), userData .slice(11) .toString('ascii') .split('\0') .shift() || '', userData.readUIntLE(2, 1), userData .subarray(3, 3 + 8) .toString('ascii') .split('\0') .shift() || '', userData.readUIntLE(39, 1), userData .slice(48, 48 + 9) .toString('ascii') .split('\0') .shift() || '', userData.readUIntLE(35, 4)); }; const decodeRecordData40 = (recordData) => { return new Attendance(recordData.readUIntLE(0, 2), recordData .slice(2, 2 + 9) .toString('ascii') .split('\0') .shift() || '', parseTimeToDate(recordData.readUInt32LE(27)), recordData.readUIntLE(26, 1), recordData.readUIntLE(31, 1)); }; const decodeRecordData16 = (recordData) => { return { user_id: recordData.readUIntLE(0, 2).toString(), record_time: parseTimeToDate(recordData.readUInt32LE(4)) }; }; const decodeRecordRealTimeLog18 = (recordData) => { const user_id = recordData.readUIntLE(8, 1).toString(); const record_time = parseHexToTime(recordData.subarray(12, 18)); return { user_id, record_time }; }; const decodeRecordRealTimeLog52 = (recordData) => { const payload = removeTcpHeader(recordData); const recvData = payload.subarray(8); const user_id = recvData.slice(0, 9) .toString('ascii') .split('\0') .shift() || ''; const record_time = parseHexToTime(recvData.subarray(26, 26 + 6)); return { user_id, record_time }; }; const decodeUDPHeader = (header) => { return { commandId: header.readUIntLE(0, 2), checkSum: header.readUIntLE(2, 2), sessionId: header.readUIntLE(4, 2), replyId: header.readUIntLE(6, 2) }; }; const decodeTCPHeader = (header) => { const recvData = header.subarray(8); const payloadSize = header.readUIntLE(4, 2); return { commandId: recvData.readUIntLE(0, 2), checkSum: recvData.readUIntLE(2, 2), sessionId: recvData.readUIntLE(4, 2), replyId: recvData.readUIntLE(6, 2), payloadSize }; }; const exportErrorMessage = (commandValue) => { const keys = Object.keys(COMMANDS); for (const key of keys) { if (COMMANDS[key] === commandValue) { return key.toString(); } } return 'AN UNKNOWN ERROR'; }; const checkNotEventTCP = (data) => { try { const cleanedData = removeTcpHeader(data); const commandId = cleanedData.readUIntLE(0, 2); const event = cleanedData.readUIntLE(4, 2); return event === COMMANDS.EF_ATTLOG && commandId === COMMANDS.CMD_REG_EVENT; } catch (err) { log(`[228] : ${err.toString()} ,${data.toString('hex')} `); return false; } }; const checkNotEventUDP = (data) => { const { commandId } = decodeUDPHeader(data.subarray(0, 8)); return commandId === COMMANDS.CMD_REG_EVENT; }; const makeKey = (key, sessionId) => { let k = 0; for (let i = 0; i < 32; i++) { if ((key & (1 << i)) !== 0) { k = (k << 1) | 1; } else { k = k << 1; } } k += sessionId; let hex = k.toString(16).padStart(8, "0"); let response = new Uint8Array(4); let index = 3; while (hex.length > 0) { response[index] = parseInt(hex.substring(0, 2), 16); index--; hex = hex.substring(2); } response[0] ^= 'Z'.charCodeAt(0); response[1] ^= 'K'.charCodeAt(0); response[2] ^= 'S'.charCodeAt(0); response[3] ^= 'O'.charCodeAt(0); let finalKey = response[0] + (response[1] << 8) + (response[2] << 16) + (response[3] << 24); let swp = finalKey >>> 16; finalKey = (finalKey << 16) | swp; return finalKey >>> 0; }; const authKey = (comKey, sessionId) => { let k = makeKey(comKey, sessionId) >>> 0; let rand = Math.floor(Math.random() * 256); let hex = k.toString(16).padStart(8, "0"); let response = new Uint8Array(4); let index = 3; while (index >= 0) { response[index] = parseInt(hex.substring(0, 2), 16); index--; hex = hex.substring(2); } response[0] ^= rand; response[1] ^= rand; response[2] = rand; response[3] ^= rand; return Array.from(response); }; /** * Represents a fingerprint template with associated metadata */ class Finger { uid; fid; valid; template; size; mark; /** * Creates a new Finger instance * @param uid User internal reference * @param fid Finger ID (value >= 0 && value <= 9) * @param valid Flag indicating 0 = invalid | 1 = valid | 3 = duress * @param template Fingerprint template data buffer */ constructor(uid, fid, valid, template) { this.uid = Number(uid); this.fid = Number(fid); this.valid = Number(valid); this.template = template; this.size = template.length; // Create mark showing first and last 8 bytes as hex const start = Uint8Array.prototype.slice.call(template, 0, 8).toString('hex'); const end = Uint8Array.prototype.slice.call(template, -8).toString('hex'); this.mark = `${start}...${end}`; } /** * Packs the fingerprint data with metadata into a Buffer * @returns Buffer containing packed fingerprint data */ repack() { // pack("HHbb%is" % (self.size), self.size+6, self.uid, self.fid, self.valid, self.template) const buf = Buffer.alloc(6 + this.size); // HHbb = 6 bytes + template size let offset = 0; buf.writeUInt16LE(this.size + 6, offset); offset += 2; buf.writeUInt16LE(this.uid, offset); offset += 2; buf.writeUInt8(this.fid, offset); offset += 1; buf.writeUInt8(this.valid, offset); offset += 1; this.template.copy(buf, offset); return buf; } /** * Packs only the fingerprint template data into a Buffer * @returns Buffer containing just the template data */ repackOnly() { // pack("H%is" % (self.size), self.size, self.template) const buf = Buffer.alloc(2 + this.size); // H = 2 bytes + template size buf.writeUInt16LE(this.size, 0); this.template.copy(buf, 2); return buf; } /** * Compares this fingerprint with another for equality * @param other Another Finger instance to compare with * @returns true if all properties and template data match */ equals(other) { if (!(other instanceof Finger)) return false; return this.uid === other.uid && this.fid === other.fid && this.valid === other.valid && this.template.equals(other.template); } } /** * Error types for device communication */ const ERROR_TYPES = { ECONNRESET: 'ECONNRESET', ECONNREFUSED: 'ECONNREFUSED'}; /** * Custom error class for device communication errors */ class ZkError { err; ip; command; /** * Creates a new ZkError instance * @param err The error object * @param command The command that caused the error * @param ip The IP address of the device */ constructor(err, command, ip) { this.err = err; this.ip = ip; this.command = command; } /** * Gets a user-friendly error message * @returns A formatted error message */ toast() { if (this.err.code === ERROR_TYPES.ECONNRESET) { return 'Another device is connecting to the device so the connection is interrupted'; } else if (this.err.code === ERROR_TYPES.ECONNREFUSED) { return 'IP of the device is refused'; } return this.err.message; } /** * Gets detailed error information * @returns An object containing error details */ getError() { return { err: { message: this.err.message, code: this.err.code }, ip: this.ip, command: this.command }; } } 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([]); 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; } 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) { 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 let timer = null; // Handle incoming data const onData = (data) => { // Check if the socket is still valid before trying to remove the listener if (this.socket) { this.socket.removeListener('data', onData); // Remove the data event listener } clearTimeout(timer); // Clear the timeout once data is received resolve(data); // Resolve the promise with the received data }; // Attach the data event listener this.socket.once('data', onData); // Attempt to write the message to the socket this.socket.write(msg, null, (err) => { if (err) { // Check if the socket is still valid before trying to remove the listener if (this.socket) { this.socket.removeListener('data', onData); // Clean up listener on write error } return reject(err); // Reject the promise with the write error } // If a timeout is set, configure it if (this.timeout) { timer = setTimeout(() => { // Check if the socket is still valid before trying to remove the listener if (this.socket) { this.socket.removeListener('data', onData); // Remove listener on timeout } reject(new Error('TIMEOUT_ON_WRITING_MESSAGE')); // Reject the promise on timeout }, connect ? 2000 : this.timeout); } }); }); } 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 }; // 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("linea 232: replyId: ", header.replyId, " command: ", header.commandId, Object.keys(COMMANDS).find(c => COMMANDS[c] == header.commandId)); } // 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 = setTimeout(() => { if (this.socket) { this.socket.removeListener('data', handleOnData); // Clean up listener on timeout } reject(new Error('TIMEOUT_ON_RECEIVING_REQUEST_DATA')); // Reject on timeout }, this.timeout); // 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, null, (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 = 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); }); } 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 buf = createTCPHeader(command, this.sessionId, this.replyId, data); try { // Write the message to the socket and wait for a response const reply = await this.writeMessage(buf, command === COMMANDS.CMD_CONNECT || command === COMMANDS.CMD_EXIT); // Remove TCP header from the response const rReply = removeTcpHeader(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); } return rReply; } catch (err) { // Log or handle the error if necessary console.error('Error executing command:', err); throw 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 { await new Promise((resolve, reject) => { this.socket.write(buf, null, (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 } }); }); } 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 = null) { return new Promise(async (resolve, reject) => { this.replyId++; const buf = createTCPHeader(COMMANDS.CMD_DATA_WRRQ, this.sessionId, this.replyId, reqData); let reply = null; 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 let remain = size % Constants.MAX_CHUNK; let 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); 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))); } } }); } /** * reject error when starting request data * @return {Record<string, User[] | Error>} when receiving requested data */ async getUsers() { try { // Free any existing buffer data to prepare for a new request if (this.socket) { await this.freeData(); } // Request user data const data = await this.readWithBuffer(REQUEST_DATA.GET_USERS); // Free buffer data after receiving the data if (this.socket) { await this.freeData(); } // Constants for user data processing const USER_PACKET_SIZE = 72; // Ensure data.data is a valid buffer if (!data.data || !(data.data instanceof Buffer)) { throw new Error('Invalid data received'); } let userData = data.data.subarray(4); // Skip the first 4 bytes (headers) const users = []; // Process each user packet while (userData.length >= USER_PACKET_SIZE) { // Decode user data and add to the users array const user = decodeUserData72(userData.subarray(0, USER_PACKET_SIZE)); users.push(user); userData = userData.subarray(USER_PACKET_SIZE); // Move to the next packet } // Return the list of users return { data: users }; } catch (err) { // Log the error for debugging console.error('Error getting users:', err); // Re-throw the error to be handled by the caller throw err; } } /** * * @param {*} ip * @param {*} callbackInProcess * reject error when starting request data * return { data: records, err: Error } when receiving requested data */ async getAttendances(callbackInProcess = () => { }) { try { // Free any existing buffer data to prepare for a new request if (this.socket) { await this.freeData(); } // Request attendance logs and handle chunked data const data = await this.readWithBuffer(REQUEST_DATA.GET_ATTENDANCE_LOGS, callbackInProcess); // Free buffer data after receiving the attendance logs if (this.socket) { await this.freeData(); } // Constants for record processing const RECORD_PACKET_SIZE = 40; // Ensure data.data is a valid buffer if (!data.data || !(data.data instanceof Buffer)) { throw new Error('Invalid data received'); } // Process the record data let recordData = data.data.subarray(4); // Skip header const records = []; // Process each attendance record while (recordData.length >= RECORD_PACKET_SIZE) { const record = decodeRecordData40(recordData.subarray(0, RECORD_PACKET_SIZE)); records.push({ ...record, ip: this.ip }); // Add IP address to each record recordData = recordData.subarray(RECORD_PACKET_SIZE); // Move to the next packet } // Return the list of attendance records return { data: records }; } catch (err) { // Log and re-throw the error console.error('Error getting attendance records:', err); throw err; // Re-throw the error for handling by the caller } } 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,