Lint and fix tests

parent 09517818
Pipeline #287 failed with stages
in 116 minutes and 19 seconds
......@@ -5,10 +5,22 @@ export const HEADER_SIZE = HEADER_MAGIC.length;
export const FOOTER_SIZE = FOOTER_MAGIC.length;
export default class CFLBinaryPList {
/**
* Compose JavaScript object into equivalent plist.
*
* @param {*} object
* @return {string} data
*/
static compose(object) {
return CFLBinaryPListComposer.compose(object);
}
/**
* Parse plist data into equivalent JavaScript built in.
*
* @param {string} data
* @return {*}
*/
static parse(data) {
return CFLBinaryPListParser.parse(data);
}
......@@ -18,7 +30,8 @@ export class CFLBinaryPListComposer {
/**
* Compose JavaScript object into equivalent plist.
*
* @return string data
* @param {*} object
* @return {string} data
*/
static compose(object) {
let data = HEADER_MAGIC;
......@@ -33,7 +46,9 @@ export class CFLBinaryPListComposer {
/**
* Pack a supported JavaScript built in object.
*
* @return string data
* @param {*} object
* @param {number} depth
* @return {string} data
*/
static packObject(object, depth = 1) {
let data = '';
......@@ -43,7 +58,8 @@ export class CFLBinaryPListComposer {
} else if (typeof object === 'boolean') {
data += object ? '\x09' : '\x08';
} else if (typeof object === 'number' && object % 1 !== 0) {
let string = '', size = null;
let string = '';
let size = null;
const sizes = {4: 'FloatBE', 8: 'DoubleBE'};
for (size of Object.keys(sizes)) {
......@@ -69,7 +85,8 @@ export class CFLBinaryPListComposer {
data += String.fromCharCode(marker);
data += string;
} else if (typeof object === 'number') {
let string = '', size = null;
let string = '';
let size = null;
const sizes = {1: '8', 2: '16BE', 4: '32BE'};
for (size of Object.keys(sizes)) {
......@@ -104,8 +121,8 @@ export class CFLBinaryPListComposer {
data += object.toString('binary');
} else if (typeof object === 'string') {
data += '\x70';
data += Buffer.from(object, 'utf8').toString('binary');
data += '\x70';
data += Buffer.from(object, 'utf8').toString('binary');
data += '\x00';
} else if (typeof object === 'object' && object instanceof Array || object instanceof Set) {
data += '\xa0';
......@@ -147,7 +164,8 @@ export class CFLBinaryPListParser {
/**
* Parse plist data into equivalent JavaScript built in.
*
* @return any
* @param {string} data
* @return {*}
*/
static parse(data) {
if (data.length < HEADER_SIZE + FOOTER_SIZE + 1) {
......@@ -175,14 +193,17 @@ export class CFLBinaryPListParser {
/**
* Unpack an object from the provided data.
*
* @return array
* @param {string} data
* @param {number} depth
* @return {Array}
*/
static unpackObject(data, depth = 1) {
if (depth > 10) {
throw new Error('Max depth reached');
}
let object = null, marker;
// let object = null;
let marker;
[marker, data] = this.unpackObjectMarker(data);
const object_type = marker & 0xf0;
......@@ -269,7 +290,8 @@ export class CFLBinaryPListParser {
const object = {};
while (true) {
let key, value;
let key;
let value;
[key, data] = this.unpackObject(data, depth + 1);
if (key === null) break;
......@@ -288,7 +310,8 @@ export class CFLBinaryPListParser {
/**
* Unpack an object marker from the provided data.
*
* @return array
* @param {string} data
* @return {Array}
*/
static unpackObjectMarker(data) {
const marker_byte = data.substr(0, 1);
......@@ -301,7 +324,9 @@ export class CFLBinaryPListParser {
/**
* Unpack an int object as a JavaScript number from the provided data.
*
* @return array
* @param {number} size_exponent
* @param {string} data
* @return {Array}
*/
static unpackInt(size_exponent, data) {
const int_size = 2 ** size_exponent;
......@@ -314,7 +339,9 @@ export class CFLBinaryPListParser {
/**
* Unpack a real object as a JavaScript number from the provided data.
*
* @return array
* @param {number} size_exponent
* @param {string} data
* @return {Array}
*/
static unpackReal(size_exponent, data) {
const real_size = 2 ** size_exponent;
......@@ -333,7 +360,9 @@ export class CFLBinaryPListParser {
/**
* Unpack count from object info nibble and/or packed int value.
*
* @return array
* @param {number} object_info
* @param {string} data
* @return {Array}
*/
static unpackCount(object_info, data) {
if (object_info === 0x0f) {
......
......@@ -126,4 +126,11 @@ yargs.command('setprop <prop> <value>', 'Set an ACP property', yargs => {
console.log(props);
}));
yargs.command('features', 'Get supported features', yargs => {}, commandHandler(async (client, argv) => {
const features = await client.getFeatures();
console.log(features);
}));
// eslint-disable-next-line no-unused-vars
const argv = yargs.argv;
......@@ -5,6 +5,14 @@ import Property, {HEADER_SIZE as ELEMENT_HEADER_SIZE} from './property';
import CFLBinaryPList from './cflbinary';
export default class Client {
/**
* Creates an ACP Client.
*
* @param {string} host
* @param {number} port
* @param {string} password
* @return {undefined}
*/
constructor(host, port, password) {
this.host = host;
this.port = port;
......@@ -13,26 +21,59 @@ export default class Client {
this.session = new Session(host, port, password);
}
/**
* Connects to the ACP server.
*
* @param {number} timeout
* @return {Promise}
*/
connect(timeout) {
return this.session.connect(timeout);
}
/**
* Disconnects from the ACP server.
*
* @return {Promise}
*/
disconnect() {
return this.session.close();
}
/**
* Sends a Message to the ACP server.
*
* @param {Message|Buffer|string} data
* @return {Promise}
*/
send(data) {
return this.session.send(data);
}
/**
* Receives data from the ACP server.
*
* @param {number} size
* @return {Promise<string>}
*/
receive(size) {
return this.session.receive(size);
}
/**
* Receives a message header from the ACP server.
*
* @return {Promise<string>}
*/
receiveMessageHeader() {
return this.receive(MESSAGE_HEADER_SIZE);
}
/**
* Receives a property element header from the ACP server.
*
* @return {Promise<string>}
*/
receivePropertyElementHeader() {
return this.receive(ELEMENT_HEADER_SIZE);
}
......@@ -43,11 +84,14 @@ export default class Client {
* Client: GetProp {...Property}
* Server: GetProp
* Server: ...Property
*
* @param {Array} props
* @return {Array}
*/
async getProperties(prop_names) {
async getProperties(props) {
let payload = '';
for (let name of prop_names) {
for (let name of props) {
payload += Property.composeRawElement(0, name instanceof Property ? name : new Property(name));
}
......@@ -61,7 +105,7 @@ export default class Client {
throw new Error('Error ' . reply_header.error_code);
}
const props = [];
const props_with_values = [];
while (true) {
const prop_header = await this.receivePropertyElementHeader();
......@@ -85,12 +129,18 @@ export default class Client {
console.debug('Prop', prop);
props.push(prop);
props_with_values.push(prop);
}
return props;
return props_with_values;
}
/**
* Sets properties on the AirPort device.
*
* @param {Array} props
* @return {undefined}
*/
async setProperties(props) {
let payload = '';
......@@ -127,6 +177,11 @@ export default class Client {
}
}
/**
* Gets the supported features on the AirPort device.
*
* @return {Array}
*/
async getFeatures() {
await this.send(Message.composeFeatCommand(0));
const reply_header = await Message.parseRaw(await this.receiveMessageHeader());
......
......@@ -3,7 +3,6 @@
* ACP message composition and parsing
*/
import Property from './property';
import {generateACPKeystream} from './keystream';
import adler32 from 'adler32';
......@@ -11,8 +10,9 @@ import adler32 from 'adler32';
/**
* Encrypt password for ACP message header key field.
* Truncates the password at 0x20 bytes, not sure if this is the right thing to use in all cases.
* @param {String} password Base station password
* @return {String} Encrypted password of proper length for the header field
*
* @param {string} password Base station password
* @return {string} Encrypted password of proper length for the header field
*/
export function generateACPHeaderKey(password) {
const password_length = 0x20;
......@@ -32,6 +32,19 @@ export const HEADER_MAGIC = 'acpp';
export const HEADER_SIZE = 128;
export default class Message {
/**
* Creates a Message.
*
* @param {number} version
* @param {number} flags
* @param {number} unused
* @param {number} command
* @param {number} error_code
* @param {string} key
* @param {string} body
* @param {number} body_size
* @return {undefined}
*/
constructor(version, flags, unused, command, error_code, key, body, body_size) {
this.version = version;
this.flags = flags;
......@@ -62,8 +75,16 @@ export default class Message {
+ 'Key: ' + this.key;
}
/**
* Unpacks an ACP message header.
*
* @param {string} header_data
* @return {object}
*/
static unpackHeader(header_data) {
if (header_data.length !== HEADER_SIZE) throw new Error('Header data must be 128 characters');
if (header_data.length !== HEADER_SIZE) {
throw new Error('Header data must be 128 bytes');
}
const buffer = Buffer.from(header_data, 'binary');
......@@ -83,6 +104,12 @@ export default class Message {
return {magic, version, header_checksum, body_checksum, body_size, flags, unused, command, error_code, key, pad1, pad2};
}
/**
* Packs an ACP message header.
*
* @param {object} header_data
* @return {string}
*/
static packHeader(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);
......@@ -103,8 +130,15 @@ export default class Message {
return buffer.toString('binary');
}
static async parseRaw(data, return_remaining) {
if (HEADER_SIZE > data.length) {
/**
* Parses a full ACP message.
*
* @param {string} data
* @param {boolean} return_remaining Whether to return any additional data
* @return {Message|Array}
*/
static parseRaw(data, return_remaining) {
if (data.length < HEADER_SIZE) {
throw new Error(`Need to pass at least ${HEADER_SIZE} bytes`);
}
......@@ -215,6 +249,11 @@ export default class Message {
return new Message(version, flags, unused, command, error_code, generateACPHeaderKey(password), payload, payload_size);
}
/**
* Composes a full ACP message.
*
* @return {string}
*/
composeRawPacket() {
let reply = this.composeHeader();
......@@ -223,6 +262,13 @@ export default class Message {
return reply;
}
/**
* Composes an ACP message header.
*
* @param {number} size
* @param {number} timeout
* @return {Promise<string>}
*/
composeHeader() {
const tmphdr = this.constructor.packHeader({
magic: HEADER_MAGIC, version: this.version,
......
......@@ -26,6 +26,13 @@ export const props = generateACPProperties();
export const HEADER_SIZE = 12;
export default class Property {
/**
* Creates a Property.
*
* @param {string} name
* @param {string} value
* @return {undefined}
*/
constructor(name, value) {
if (name === '\x00\x00\x00\x00' && value === '\x00\x00\x00\x00') {
name = undefined;
......@@ -120,6 +127,11 @@ export default class Property {
}
}
/**
* Convert the property's value to a JavaScript built in type.
*
* @return {*}
*/
format() {
if (!this.name || !this.value) return '';
......@@ -167,9 +179,14 @@ export default class Property {
}
toString() {
return this.format();
return JSON.stringify(this.format());
}
/**
* Returns the names of known properties.
*
* @return {Array}
*/
static getSupportedPropertyNames() {
return props.map(prop => prop.name);
}
......@@ -192,17 +209,27 @@ export default class Property {
return prop[key];
}
static async parseRawElement(data) {
/**
* Parses an ACP property.
*
* @param {string} data
* @return {Property}
*/
static parseRawElement(data) {
// eslint-disable-next-line no-unused-vars
const {name, flags, size} = await this.parseRawElementHeader(data.substr(0, HEADER_SIZE));
const {name, flags, size} = this.unpackHeader(data.substr(0, HEADER_SIZE));
// TODO: handle flags
return new this(name, data.substr(HEADER_SIZE));
}
static async parseRawElementHeader(data) {
return this.unpackHeader(data);
}
/**
* Composes an ACP property.
*
* @param {number} flags
* @param {Property} property
* @return {string}
*/
static composeRawElement(flags, property) {
const name = property.name ? property.name : '\x00\x00\x00\x00';
const value = property.value ? property.value : '\x00\x00\x00\x00';
......@@ -228,6 +255,12 @@ export default class Property {
}
}
/**
* Packs an ACP property header.
*
* @param {object} header_data
* @return {string}
*/
static packHeader(header_data) {
const {name, flags, size} = header_data;
const buffer = Buffer.alloc(12);
......@@ -239,6 +272,12 @@ export default class Property {
return buffer.toString('binary');
}
/**
* Unpacks an ACP property header.
*
* @param {string} header_data
* @return {object}
*/
static unpackHeader(header_data) {
if (header_data.length !== HEADER_SIZE) {
throw new Error('Header data must be 12 characters');
......
......@@ -2,7 +2,7 @@
import Session from './session';
import Message, {HEADER_SIZE as MESSAGE_HEADER_SIZE} from './message';
import Property, {HEADER_SIZE as ELEMENT_HEADER_SIZE} from './property';
import CFLBinaryPList from './cflbinary';
// import CFLBinaryPList from './cflbinary';
import net from 'net';
......@@ -96,44 +96,46 @@ export default class Server {
console.log('Received message', message);
switch (message.command) {
// Get prop
case 0x14: {
let data = message.body;
const props = [];
// Get prop
case 0x14: {
let data = message.body;
const props = [];
// Read the requested props into an array of Propertys
while (data.length) {
const prop_header = data.substr(0, ELEMENT_HEADER_SIZE);
const prop_data = await Property.parseRawElementHeader(prop_header);
console.debug(prop_data);
const {name, flags, size} = prop_data;
// Read the requested props into an array of Propertys
while (data.length) {
const prop_header = data.substr(0, ELEMENT_HEADER_SIZE);
const prop_data = await Property.parseRawElementHeader(prop_header);
console.debug(prop_data);
const {name, size} = prop_data;
const value = data.substr(0, ELEMENT_HEADER_SIZE + size);
data = data.substr(ELEMENT_HEADER_SIZE + size);
const value = data.substr(0, ELEMENT_HEADER_SIZE + size);
data = data.substr(ELEMENT_HEADER_SIZE + size);
const prop = new Property(name, value);
const prop = new Property(name, value);
if (typeof prop.name === 'undefined' && typeof prop.value === 'undefined') {
break;
}
props.push(prop);
if (typeof prop.name === 'undefined' && typeof prop.value === 'undefined') {
break;
}
// Send back an array of Propertys
const ret = this.getProperties(props);
let payload = '', i = 0;
props.push(prop);
}
for (let prop of ret) {
payload += Property.composeRawElement(0, prop instanceof Property ? prop : new Property(props[i], prop));
i++;
}
// Send back an array of Propertys
const ret = this.getProperties(props);
const response = Message.composeGetPropCommand(4, this.password, payload);
let payload = '';
let i = 0;
return;
for (let prop of ret) {
payload += Property.composeRawElement(0, prop instanceof Property ? prop : new Property(props[i], prop));
i++;
}
// eslint-disable-next-line no-unused-vars
const response = Message.composeGetPropCommand(4, this.password, payload);
return;
}
}
}
......
import Message, {HEADER_SIZE as MESSAGE_HEADER_SIZE} from './message';
import Property, {HEADER_SIZE as ELEMENT_HEADER_SIZE} from './property';
import {HEADER_SIZE as ELEMENT_HEADER_SIZE} from './property';
// import {ClientEncryption, ServerEncryption} from './encryption';
import net from 'net';
export default class Session {
/**
* Creates a Session.
*
* @param {string} host
* @param {number} port
* @param {string} password
* @return {undefined}
*/
constructor(host, port, password) {
this.host = host;
this.port = port;
......@@ -18,14 +26,20 @@ export default class Session {
this.encryption = undefined;
}
connect(_timeout = 10000) {
/**
* Connects to the ACP server.
*
* @param {number} timeout
* @return {Promise}
*/
connect(timeout = 10000) {
return new Promise((resolve, reject) => {
this.socket = new net.Socket();
setTimeout(() => {
this.reading -= 1;
reject('Timeout');
}, _timeout);
}, timeout);
this.socket.connect(this.port, this.host, err => {
console.log('Connected', err);
......@@ -45,16 +59,30 @@ export default class Session {
});
}
/**
* Disconnects from the ACP server.
*
* @return {Promise}
*/
close() {
if (!this.socket) return;
this.socket.end();
return new Promise((resolve, reject) => {
this.socket.on('close', resolve);
this.socket.on('close', () => {
this.socket = undefined;
resolve();
});
});
}
/**
* Receives and parses a Message from the ACP server.
*
* @param {number} timeout
* @return {Promise<Message>}
*/
async receiveMessage(timeout) {
const raw_header = await this.receiveMessageHeader(timeout);
const message = await Message.parseRaw(raw_header);
......@@ -66,20 +94,46 @@ export default class Session {
return message;
}
/**
* Receives a message header from the ACP server.
*
* @param {number} timeout
* @return {Promise<string>}
*/
receiveMessageHeader(timeout) {
return this.receive(MESSAGE_HEADER_SIZE, timeout);
}
/**
* Receives a property element header from the ACP server.
*
* @param {number} timeout
* @return {Promise<string>}
*/
receivePropertyElementHeader(timeout) {
return this.receive(ELEMENT_HEADER_SIZE, timeout);
}
/**
* Sends and receives data to/from the ACP server.
*
* @param {Message|Buffer|string} data
* @param {number} size
* @param {number} timeout
* @return {Promise<string>}
*/
async sendAndReceive(data, size, timeout = 10000) {
await this.send(data);
return await this.receive(size, timeout);
}
/**
* Sends data to the ACP server.
*
* @param {Message|Buffer|string} data
* @return {Promise}
*/
send(data) {
if (data instanceof Message) {
data = data.composeRawPacket();
......@@ -104,7 +158,14 @@ export default class Session {
});
}
receiveSize(size, _timeout = 10000) {
/**
* Receives raw data from the ACP server.
*
* @param {number} size
* @param {number} timeout
* @return {Promise<string>}
*/
receiveSize(size, timeout = 10000) {
const receivedChunks = [this.buffer.substr(0, size)];
this.buffer = this.buffer.substr(size);
this.reading++;
......@@ -115,7 +176,7 @@ export default class Session {
return Promise.resolve(receivedChunks.join(''));
}
let timeout;
let _timeout = timeout;
return new Promise((resolve, reject) => {
const defer = () => {
......@@ -123,7 +184,7 @@ export default class Session {
reject('Timeout');
};
timeout = setTimeout(defer, _timeout);
let timeout = setTimeout(defer, _timeout);
const listener = data => {
data = data.toString('binary');
......@@ -159,6 +220,13 @@ export default class Session {
});
}
/**
* Receives and decrypts data from the ACP server.
*
* @param {number} size
* @param {number} timeout
* @return {Promise<string>}
*/
async receive(size, timeout = 10000) {