...
 
Commits (6)
......@@ -4,7 +4,7 @@
"env",
{
"targets": {
"node": "6.10"
"node": "8.10"
}
}
]
......
# This file is a template, and might need editing before it works on your project.
# Official framework image. Look for the different tagged releases at:
# https://hub.docker.com/r/library/node/tags/
image: node:latest
# This folder is cached between builds
# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
cache:
paths:
- node_modules/
build:
script:
- npm install
- npx gulp build
artifacts:
paths:
- dist
test:
script:
- npx qunit
import gulp from 'gulp';
import pump from 'pump';
import babel from 'gulp-babel';
gulp.task('build', function () {
return pump([
gulp.src('src/**.js'),
babel(),
gulp.dest('dist')
]);
});
gulp.task('watch', gulp.series('build', function () {
return gulp.watch('src/**.js', gulp.series('build'));
}));
const gulp = require('gulp');
const pump = require('pump');
const babel = require('gulp-babel');
const plumber = require('gulp-plumber');
gulp.task('build', function () {
return pump([
gulp.src('src/**/*.js'),
plumber(),
babel(),
gulp.dest('dist')
]);
});
gulp.task('build-tests', function () {
return pump([
gulp.src('tests/**/*.js'),
plumber(),
babel(),
gulp.dest('tests-dist')
]);
});
gulp.task('watch', gulp.series('build', function () {
return gulp.watch('src/**/*.js', gulp.series('build'));
}));
gulp.task('watch-tests', gulp.series('build-tests', function () {
return gulp.watch('src/**/*.js', gulp.series('build-tests'));
}));
gulp.task('build-all', gulp.parallel('build', 'build-tests'));
gulp.task('watch-all', gulp.parallel('watch', 'watch-tests'));
This diff is collapsed.
......@@ -10,8 +10,7 @@
},
"main": "dist/client.js",
"dependencies": {
"adler32": "^0.1.7",
"python-struct": "^1.0.6"
"adler32": "^0.1.7"
},
"devDependencies": {
"babel-core": "^6.26.3",
......@@ -21,7 +20,7 @@
"eslint-config-google": "^0.9.1",
"gulp": "^4.0.0",
"gulp-babel": "^7.0.1",
"gulp-plumber": "^1.2.0",
"pump": "^3.0.0"
"pump": "^3.0.0",
"qunit": "^2.8.0"
}
}
import Session from './session';
import Message from './message';
import Property from './property';
import Property, { elementHeaderSize } from './property';
// import { CFLBinaryPListParser } from './cflbinary';
import struct from 'python-struct';
export default class Client {
constructor(host, port, password) {
......@@ -37,7 +35,7 @@ export default class Client {
}
receivePropertyElementHeader() {
return this.receive(Property.elementHeaderSize);
return this.receive(elementHeaderSize);
}
async getProperties(prop_names) {
......@@ -74,8 +72,8 @@ export default class Client {
const propData = await this.receive(size);
console.debug('prop data', propData);
if (flags) {
const { errorCode } = struct.unpack('>I', Buffer.from(propData, 'binary'));
if (flags & 1) {
const errorCode = Buffer.from(propData, 'binary').readInt32BE(0);
console.log('error requesting value for property', name, '-', errorCode);
continue;
}
......@@ -122,7 +120,7 @@ export default class Client {
console.debug('prop data', propData);
if (flags) {
const { errorCode } = struct.unpack('>I', Buffer.from(propData, 'binary'));
const [errorCode] = Buffer.from(propData, 'binary').readUInt32BE(0);
console.log('error setting value for property', name, '-', errorCode);
return;
}
......
......@@ -3,7 +3,7 @@
* Static key/seed for keystream generation
*/
export const ACP_STATIC_KEY = Buffer.from('5b6faf5d9d5b0e1351f2da1de7e8d673', 'hex').toString('utf8');
export const ACP_STATIC_KEY = Buffer.from('5b6faf5d9d5b0e1351f2da1de7e8d673', 'hex').toString('binary');
export function generateACPKeystream(length) {
let key = '';
......@@ -11,6 +11,7 @@ export function generateACPKeystream(length) {
while (key_idx < length) {
key += String.fromCharCode((key_idx + 0x55 & 0xFF) ^ ACP_STATIC_KEY.charCodeAt(key_idx % ACP_STATIC_KEY.length));
key_idx++;
}
......
......@@ -4,7 +4,6 @@
*/
import { generateACPKeystream } from './keystream';
import struct from 'python-struct';
import adler32 from 'adler32';
/**
......@@ -17,7 +16,7 @@ export function generateACPHeaderKey(password) {
const passwordLength = 0x20;
const passwordKey = generateACPKeystream(passwordLength);
const passwordBuffer = password.substr(0, passwordLength).padStart(passwordLength, '\x00');
const passwordBuffer = password.substr(0, passwordLength).padEnd(passwordLength, '\x00');
let encryptedPasswordBuffer = '';
for (let i = 0; i < passwordLength; i++) {
encryptedPasswordBuffer += String.fromCharCode(passwordKey.charCodeAt(i) ^ passwordBuffer.charCodeAt(i));
......@@ -26,7 +25,6 @@ export function generateACPHeaderKey(password) {
return encryptedPasswordBuffer;
}
export const headerFormat = '!4s8i12x32s48x';
export const headerMagic = 'acpp';
export const headerSize = 128;
......@@ -44,7 +42,7 @@ export default class Message {
this.bodyChecksum = 1;
} else {
this.bodySize = typeof bodySize !== 'undefined' ? bodySize : body.length;
this.bodyChecksum = adler32.sum(body);
this.bodyChecksum = adler32.sum(Buffer.from(body, 'binary'));
}
this.key = key;
......@@ -64,6 +62,53 @@ export default class Message {
return s;
}
static unpackHeader(header_data) {
if (header_data.length !== headerSize) throw new Error('Header data must be 128 characters');
console.log('Unpacking header data', header_data);
const buffer = Buffer.from(header_data, 'binary');
const magic = buffer.slice(0, 4).toString();
const version = buffer.readInt32BE(4);
const header_checksum = buffer.readUInt32BE(8);
const body_checksum = buffer.readInt32BE(12);
const body_size = buffer.readInt32BE(16);
const flags = buffer.readInt32BE(20);
const unused = buffer.readInt32BE(24);
const command = buffer.readInt32BE(28);
const error_code = buffer.readInt32BE(32);
const pad1 = buffer.slice(36, 36 + 12).toString('binary');
const key = buffer.slice(48, 48 + 32).toString('binary');
const pad2 = buffer.slice(80, 80 + 48).toString('binary');
const unpacked = {packed: header_data, magic, version, header_checksum, body_checksum, body_size, flags, unused, command, error_code, key, pad1, pad2};
console.log('Unpacked', unpacked);
return unpacked;
}
static packHeader(header_data) {
console.log('Packing header data', header_data);
const {magic, version, header_checksum, body_checksum, body_size, flags, unused, command, error_code, key, pad1 = '', pad2 = ''} = header_data;
const buffer = Buffer.alloc(128);
buffer.write(magic, 0, 4);
buffer.writeInt32BE(version, 4);
buffer.writeInt32BE(header_checksum, 8);
buffer.writeInt32BE(body_checksum, 12);
buffer.writeInt32BE(body_size, 16);
buffer.writeInt32BE(flags, 20);
buffer.writeInt32BE(unused, 24);
buffer.writeInt32BE(command, 28);
buffer.writeInt32BE(error_code, 32);
buffer.write(header_data.pad1 || ''.padEnd(12, '\u0000'), 36, 36 + 12, 'binary');
buffer.write(key, 48, 48 + 32, 'binary');
buffer.write(header_data.pad2 || ''.padEnd(48, '\u0000'), 80, 80 + 48, 'binary');
const packed = buffer.toString('binary');
console.log('Packed', packed);
return packed;
}
static async parseRaw(data) {
if (headerSize > data.length)
throw new Error(`Need to pass at least ${headerSize} bytes`);
......@@ -71,13 +116,7 @@ export default class Message {
const header_data = data.substr(0, headerSize);
const body_data = data.length > headerSize ? data.substr(headerSize) : undefined;
const unpackedData = struct.unpack(headerFormat, Buffer.from(header_data, 'binary'));
const [magic, version, header_checksum, body_checksum, body_size, flags, unused, command, error_code, key] = unpackedData;
console.debug('ACP message header fields (parsed not validated):', {
magic, version, header_checksum, body_checksum, body_size, flags, unused, command, error_code, key
});
const {magic, version, header_checksum, body_checksum, body_size, flags, unused, command, error_code, key, pad1, pad2} = this.unpackHeader(header_data);
if (magic !== headerMagic)
throw new Error('Bad header magic');
......@@ -86,13 +125,15 @@ export default class Message {
if (!versions.includes(version))
throw new Error('Unknown version ' + version);
const tmphdr = struct.pack(headerFormat, [
const tmphdr = this.packHeader({
magic, version,
/* header_checksum: */ 0, body_checksum, body_size,
flags, unused, command, error_code, key
]).toString('binary');
header_checksum: 0, body_checksum, body_size,
flags, unused, command, error_code, key,
pad1, pad2
});
if (header_checksum !== adler32.sum(tmphdr))
const expectedHeaderChecksum = adler32.sum(Buffer.from(tmphdr, 'binary'));
if (header_checksum !== expectedHeaderChecksum)
throw new Error('Header checksum does not match');
if (body_data && body_size === -1)
......@@ -101,7 +142,7 @@ export default class Message {
if (body_data && body_size !== body_data.length)
throw new Error('Message body size does not match available data');
if (body_data && body_checksum !== adler32.sum(body_data))
if (body_data && body_checksum !== adler32.sum(Buffer.from(body_data, 'binary')))
throw new Error('Body checksum does not match');
// TODO: check flags
......@@ -162,12 +203,12 @@ export default class Message {
}
static composeAuthCommand(flags, password, payload) {
const message = new Message(0x00030001, flags, 0, 0x1a, 0, generateACPHeaderKey(password), payload);
const message = new Message(0x00030001, flags, 0, 0x1a, 0, generateACPHeaderKey(''), payload);
return message.composeRawPacket();
}
static composeFeatCommand(flags, password, payload) {
const message = new Message(0x00030001, flags, 0, 0x1b, 0, generateACPHeaderKey(password), payload);
const message = new Message(0x00030001, flags, 0, 0x1b, 0, generateACPHeaderKey(''), payload);
return message.composeRawPacket();
}
......@@ -185,23 +226,19 @@ export default class Message {
}
composeHeader() {
const tmphdr = struct.pack(headerFormat, [
headerMagic, this.version,
0, this.bodyChecksum, this.bodySize,
this.flags, this.unused, this.command, this.errorCode, this.key
]).toString('binary');
const header = struct.pack(headerFormat, [
headerMagic, this.version,
adler32.sum(tmphdr), this.bodyChecksum, this.bodySize,
this.flags, this.unused, this.command, this.errorCode, this.key
]).toString('binary');
const tmphdr = this.constructor.packHeader({
magic: headerMagic, version: this.version,
header_checksum: 0, body_checksum: this.bodyChecksum, body_size: this.bodySize,
flags: this.flags, unused: this.unused, command: this.command, error_code: this.errorCode, key: this.key
});
return header;
}
const header = this.constructor.packHeader({
magic: headerMagic, version: this.version,
header_checksum: adler32.sum(Buffer.from(tmphdr, 'binary')), body_checksum: this.bodyChecksum, body_size: this.bodySize,
flags: this.flags, unused: this.unused, command: this.command, error_code: this.errorCode, key: this.key
});
static get headerFormat() {
return headerFormat;
return header;
}
static get headerMagic() {
......
import acpProperties from './properties';
const struct = require('python-struct');
export function generateACPProperties() {
const props = [];
for (let prop of acpProperties) {
......@@ -18,8 +16,7 @@ export function generateACPProperties() {
export const props = generateACPProperties();
export const elementHeaderFormat = '!4s2I';
export const elementHeaderSize = struct.sizeOf(elementHeaderFormat);
export const elementHeaderSize = 12;
export default class Property {
......@@ -29,25 +26,22 @@ export default class Property {
value = undefined;
}
if (name && !Property.getSupportedPropertyNames().includes(name))
if (name && !this.constructor.getSupportedPropertyNames().includes(name))
throw new Error('Invalid property name passed to Property constructor: ' + name);
if (value) {
const propType = this.getPropertyInfoString(name, 'type');
const propType = this.constructor.getPropertyInfoString(name, 'type');
const initHandlerName = `__init_${propType}`;
if (!this[initHandlerName]) throw new Error(`Missing handler for ${propType} property type`);
const initHandler = this[initHandlerName];
console.debug('Old value:', value, '- type:', typeof value);
try {
value = initHandler(value);
value = this[initHandlerName](value);
} catch (err) {
throw new Error(JSON.stringify(err, null, 4) + ' provided for ' + propType + ' property type ' + value);
}
console.debug('New value:', value, '- type:', typeof value);
const validate = Property.getPropertyInfoString(name, 'validate');
const validate = this.constructor.getPropertyInfoString(name, 'validate');
if (validate && !validate(value))
throw new Error('Invalid value passed to validator for property ' + name + ' - type: ' + typeof value);
}
......@@ -60,7 +54,7 @@ export default class Property {
if (typeof value === 'number') {
return value;
} else if (typeof value === 'string') {
return parseInt(value);
return Buffer.from(value, 'binary').readUInt32BE(0);
} else {
throw new Error('Invalid number value: ' + value);
}
......@@ -70,7 +64,7 @@ export default class Property {
if (typeof value === 'number') {
return value;
} else if (typeof value === 'string') {
return value;
return Buffer.from(value, 'binary').readUInt32BE(0);
} else {
throw new Error('Invalid hex value: ' + value);
}
......@@ -120,21 +114,56 @@ export default class Property {
}
}
toString() {
format() {
if (!this.name || !this.value) return '';
const propType = this.getPropertyInfoString(this.name, 'type');
const propType = this.constructor.getPropertyInfoString(this.name, 'type');
const formatHandlerName = `__format_${propType}`;
// For now just return string value
return this.value.toString();
if (!this[formatHandlerName]) throw new Error(`Missing format handler for ${propType} property type`);
return this[formatHandlerName](this.value);
}
__format_dec(value) {
return value.toString();
}
__format_hex(value) {
return '0x' + value.toString(16);
}
__format_mac(value) {
const mac_bytes = [];
for (let i = 0; i < 6; i++) {
mac_bytes.push(value.substr(i, 1));
}
return implode(':', mac_bytes);
}
__format_bin(value) {
return value.toString();
}
__format_cfb(value) {
return value.toString();
}
__format_log(value) {
return value.toString();
}
toString() {
return this.format();
}
static getSupportedPropertyNames() {
return props.map(prop => prop.name);
}
static getPropertyInfoString(cls, propName, key) {
static getPropertyInfoString(propName, key) {
if (!propName) return;
const prop = props.find(p => p.name === propName);
......@@ -155,11 +184,11 @@ export default class Property {
static async parseRawElement(data) {
const { name, flags, size } = await this.parseRawElementHeader(data.substr(0, elementHeaderSize));
// TODO: handle flags
return new Property(name, data.substr(elementHeaderSize));
return new this(name, data.substr(elementHeaderSize));
}
static async parseRawElementHeader(data) {
return struct.unpack(elementHeaderFormat, Buffer.from(data, 'binary'));
return this.unpackHeader(data);
}
static composeRawElement(flags, property) {
......@@ -167,7 +196,10 @@ export default class Property {
const value = property.value ? property.value : '\x00\x00\x00\x00';
if (typeof value === 'number') {
return this.composeRawElementHeader(name, flags, struct.sizeOf('>I')) + struct.pack('>I', [value]);
const buffer = Buffer.alloc(4);
buffer.writeUInt32BE(value, 0);
return this.composeRawElementHeader(name, flags, 4) + buffer.toString('binary');
} else if (typeof value === 'string') {
return this.composeRawElementHeader(name, flags, value.length) + value;
} else {
......@@ -177,10 +209,35 @@ export default class Property {
static composeRawElementHeader(name, flags, size) {
try {
return struct.pack(elementHeaderFormat, [name, flags, size]).toString('binary');
return this.packHeader({name, flags, size});
} catch (err) {
console.error('Error packing', name, flags, size, '- :', err);
throw err;
}
}
static packHeader(header_data) {
const {name, flags, size} = header_data;
const buffer = Buffer.alloc(12);
buffer.write(name, 0, 4);
buffer.writeUInt32BE(flags, 4);
buffer.writeUInt32BE(size, 8);
return buffer.toString('binary');
}
static unpackHeader(header_data) {
if (header_data.length !== elementHeaderSize)
throw new Error('Header data must be 12 characters');
const buffer = Buffer.from(header_data, 'binary');
const name = buffer.slice(0, 4).toString();
const flags = buffer.readUInt32BE(4);
const size = buffer.readUInt32BE(8);
return {name, flags, size};
}
}
......@@ -38,9 +38,9 @@ export default class Session {
});
this.socket.on('data', data => {
console.debug(0, 'Receiving data', data.toString());
console.debug(0, 'Receiving data', data);
if (this.reading) return;
this.buffer += data.toString();
this.buffer += data.toString('binary');
});
});
}
......@@ -66,6 +66,9 @@ export default class Session {
}
send(data) {
if (!Buffer.isBuffer(data))
data = Buffer.from(data, 'binary');
if (this.encryptionMethod)
data = this.encryptionMethod(data);
......@@ -73,7 +76,7 @@ export default class Session {
return new Promise((resolve, reject) => {
console.info(0, 'Sending data', data);
this.socket.write(data, 'utf-8', err => {
this.socket.write(data, 'binary', err => {
if (err) reject(err);
else resolve();
});
......@@ -81,12 +84,16 @@ export default class Session {
}
receiveSize(size, _timeout = 10000) {
const receivedChunks = [this.buffer];
this.buffer = '';
const receivedChunks = [this.buffer.substr(0, size)];
this.buffer = this.buffer.substr(size);
this.reading++;
let receivedSize = this.buffer.length;
let receivedSize = receivedChunks[0].length;
let waitingFor = size - receivedSize;
if (waitingFor <= 0) {
return Promise.resolve(receivedChunks.join(''));
}
let updated = Date.now();
let timeout;
......@@ -99,14 +106,16 @@ export default class Session {
timeout = setTimeout(defer, _timeout);
const listener = data => {
data = data.toString('binary');
if (data.length > waitingFor) {
this.buffer += data.substr(waitingFor);
data = data.substr(0, waitingFor);
}
receivedChunks.push(data.toString());
receivedSize += data.toString().length;
waitingFor = waitingFor - data.toString().length;
receivedChunks.push(data);
receivedSize += data.length;
waitingFor = waitingFor - data.length;
clearTimeout(timeout);
......
const {generateACPHeaderKey} = require('../dist/message');
const {generateACPKeystream, ACP_STATIC_KEY} = require('../dist/keystream');
const QUnit = require('qunit');
QUnit.test('Generate keystream', function (assert) {
const expected_hex = '0e39f805c401554f0cac857d868ab5173e09c835';
const key = generateACPKeystream(20);
const key_hex = Buffer.from(key, 'binary').toString('hex');
assert.equal(expected_hex, key_hex);
});
QUnit.test('Generate message header key', function (assert) {
const expected_hex = '7a5c8b71ad6f324f0cac857d868ab5173e09c835f431657f3c9cb56d969aa507';
const key = generateACPHeaderKey('testing');
const key_hex = Buffer.from(key, 'binary').toString('hex');
assert.equal(expected_hex, key_hex);
});
const {default: Message, generateACPHeaderKey} = require('../dist/message');
const QUnit = require('qunit');
const adler32 = require('adler32');
QUnit.test('Pack header', function (assert) {
const expected_hex = '61637070000300010000000000000000ffffffff000000040000000000000014000000000000000000000000000000007a5c8b71ad6f324f0cac857d868ab5173e09c835f431657f3c9cb56d969aa507000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';
const message = new Message(0x00030001, 4, 0, 0x14, 0, generateACPHeaderKey('testing'));
const packed = Message.packHeader({
magic: Message.headerMagic,
version: message.version,
header_checksum: 0,
body_checksum: 0,
body_size: message.bodySize,
flags: message.flags,
unused: message.unused,
command: message.command,
error_code: message.errorCode,
key: message.key,
});
const message_hex = Buffer.from(packed, 'binary').toString('hex');
assert.equal(message_hex, expected_hex);
});
QUnit.test('Pack header with checksum', function (assert) {
const expected_hex = '6163707000030001214613e500000000ffffffff000000040000000000000014000000000000000000000000000000007a5c8b71ad6f324f0cac857d868ab5173e09c835f431657f3c9cb56d969aa507000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';
const message = new Message(0x00030001, 4, 0, 0x14, 0, generateACPHeaderKey('testing'));
const packed = Message.packHeader({
magic: Message.headerMagic,
version: message.version,
header_checksum: 0,
body_checksum: 0,
body_size: message.bodySize,
flags: message.flags,
unused: message.unused,
command: message.command,
error_code: message.errorCode,
key: message.key,
});
const header_checksum = adler32.sum(Buffer.from(packed, 'binary'));
console.log('Header checksum', header_checksum);
assert.equal(header_checksum, 558240741);
const packed_checksum = Message.packHeader({
magic: Message.headerMagic,
version: message.version,
header_checksum,
body_checksum: 0,
body_size: message.bodySize,
flags: message.flags,
unused: message.unused,
command: message.command,
error_code: message.errorCode,
key: message.key,
});
const message_hex = Buffer.from(packed_checksum, 'binary').toString('hex');
assert.equal(message_hex, expected_hex);
});
QUnit.test('Compose get prop command', function (assert) {
// Property.composeRawElement(0, new Property('dbug'))
const payload_hex = '64627567000000000000000400000000';
const payload = Buffer.from(payload_hex, 'hex').toString('binary');
const expected_hex = '61637070000300011bef117b17c301a700000010000000040000000000000014000000000000000000000000000000007a5c8b71ad6f324f0cac857d868ab5173e09c835f431657f3c9cb56d969aa50700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064627567000000000000000400000000';
const message = Message.composeGetPropCommand(4, 'testing', payload);
const message_hex = Buffer.from(message, 'binary').toString('hex');
assert.equal(message_hex, expected_hex);
});
QUnit.test('Parse raw command', async function (assert) {
const raw_message_hex = '61637070000300011bef117b17c301a700000010000000040000000000000014000000000000000000000000000000007a5c8b71ad6f324f0cac857d868ab5173e09c835f431657f3c9cb56d969aa50700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000064627567000000000000000400000000';
const raw_message = Buffer.from(raw_message_hex, 'hex').toString('binary');
const message = await Message.parseRaw(raw_message, false);
assert.equal(message.version, 196609);
assert.equal(message.flags, 4);
assert.equal(message.unused, 0);
assert.equal(message.command, 20);
assert.equal(message.errorCode, 0);
assert.equal(message.key, Buffer.from('7a5c8b71ad6f324f0cac857d868ab5173e09c835f431657f3c9cb56d969aa507', 'hex').toString('binary'));
assert.equal(message.body, Buffer.from('64627567000000000000000400000000', 'hex').toString('binary'));
assert.equal(message.bodySize, 16);
assert.equal(message.bodyChecksum, 398655911);
});
const {default: Property} = require('../dist/property');
const QUnit = require('qunit');
QUnit.test('Compose raw element header', function (assert) {
const expected_hex = '646275670000000000000004';
const header = Property.composeRawElementHeader('dbug', 0, 4);
const header_hex = Buffer.from(header, 'binary').toString('hex');
assert.equal(expected_hex, header_hex);
});
QUnit.test('Compose raw element', function (assert) {
const expected_hex = '64627567000000000000000400000000';
const raw_element = Property.composeRawElement(0, new Property('dbug'));
const raw_element_hex = Buffer.from(raw_element, 'binary').toString('hex');
assert.equal(expected_hex, raw_element_hex);
});
QUnit.test('Parse raw element', async function (assert) {
const raw_element_hex = '64627567000000000000000400003000';
const raw_element = Buffer.from(raw_element_hex, 'hex').toString('binary');
const property = await Property.parseRawElement(raw_element);
assert.equal(property.name, 'dbug');
assert.equal(property.value, 0x3000);
});
import adler32 from 'adler32';
import Client from '../';
const adler32 = require('adler32');
const {default: Client} = require('..');
const client = new Client('192.168.2.251', 5009, 'testing');
......@@ -10,8 +10,8 @@ const client = new Client('192.168.2.251', 5009, 'testing');
console.log('Connected to', client.host, client.port);
try {
const value = await client.getProperties(['dbug']);
console.log('Value:', value);
const props = await client.getProperties(['dbug']);
console.log('Value:', props, props[0].format());
} catch (err) {
console.error('Caught error:', err);
}
......