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
JavaScript
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,