Initial commit

parents
{
"presets": [
[
"env",
{
"targets": {
"node": "6.10"
}
}
]
]
}
root = true
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[*.md]
trim_trailing_whitespace = false
---
parserOptions:
ecmaVersion: 8
sourceType: module
env:
node: true
extends:
- eslint:recommended
rules:
no-unused-vars: 'off'
no-console: 'off'
no-empty: 'off'
node_modules
dist
tests-dist
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.
{
"name": "airpyrt.js",
"author": {
"name": "Samuel Elliott"
},
"version": "0.1.0",
"repository": {
"type": "git",
"url": "https://github.com/samuelthomas2774/airpyrt.js"
},
"main": "dist/index.js",
"dependencies": {
"adler32": "^0.1.7",
"python-struct": "^1.0.6"
},
"devDependencies": {
"babel-core": "^6.26.3",
"babel-preset-env": "^1.6.1",
"babel-preset-es2015": "^6.24.1",
"eslint": "^4.19.1",
"eslint-config-google": "^0.9.1",
"gulp": "^4.0.0",
"gulp-babel": "^7.0.1",
"gulp-plumber": "^1.2.0",
"pump": "^3.0.0"
}
}
import Session from './session';
import Message from './message';
import Property from './property';
// import { CFLBinaryPListParser } from './cflbinary';
import struct from 'python-struct';
export default class Client {
constructor(host, port, password) {
this.host = host;
this.port = port;
this.password = password;
this.session = new Session(host, port, password);
}
connect(timeout) {
return this.session.connect(timeout);
}
disconnect() {
return this.session.close();
}
send(data) {
return this.session.send(data);
}
receive(size) {
return this.session.receive(size);
}
receiveMessageHeader() {
return this.receive(Message.headerSize);
}
receivePropertyElementHeader() {
return this.receive(Property.elementHeaderSize);
}
async getProperties(prop_names) {
let payload = '';
for (let name of prop_names) {
payload += Property.composeRawElement(0, new Property(name));
}
const request = Message.composeGetPropCommand(4, this.password, payload);
await this.send(request);
const reply = await this.receiveMessageHeader();
const replyHeader = await Message.parseRaw(reply);
console.debug('Reply:', {
reply, replyHeader
});
if (replyHeader.errorCode !== 0) {
console.log('Client.getProperties error code:', replyHeader.errorCode);
return [];
}
const props = [];
while (true) {
const propHeader = await this.receivePropertyElementHeader();
console.debug('Received property element header:', propHeader);
const { name, flags, size } = await Property.parseRawElementHeader(propHeader);
console.debug('name', name);
console.debug('flags', flags);
console.debug('size', size);
const propData = await this.receive(size);
console.debug('prop data', propData);
if (flags) {
const { errorCode } = struct.unpack('>I', Buffer.from(propData, 'binary'));
console.log('error requesting value for property', name, '-', errorCode);
continue;
}
const prop = new Property(name, propData);
console.debug('prop', prop);
if (typeof prop.name === 'undefined' && typeof prop.value === 'undefined') {
console.debug('found empty prop end marker');
break;
}
props.push(prop);
}
return props;
}
async setProperties(props) {
let payload = '';
for (let { name, prop } of props) {
console.debug('prop', prop);
payload += Property.composeRawElement(0, prop);
}
const request = Message.composeSetPropCommand(0, this.password, payload);
this.send(request);
const rawReply = await this.receiveMessageHeader();
const replyHeader = await Message.parseRaw(rawReply);
if (replyHeader.errorCode !== 0) {
console.log('set properties error code', replyHeader.errorCode);
return;
}
const propHeader = await this.receivePropertyElementHeader();
const { name, flags, size } = await Property.parseRawElementHeader(propHeader);
console.debug('name', name);
console.debug('flags', flags);
console.debug('size', size);
const propData = await this.receive(size);
console.debug('prop data', propData);
if (flags) {
const { errorCode } = struct.unpack('>I', Buffer.from(propData, 'binary'));
console.log('error setting value for property', name, '-', errorCode);
return;
}
const prop = new Property(name, propData);
console.debug('prop', prop);
if (typeof prop.name === 'undefined' && typeof prop.value === 'undefined')
console.debug('found empty prop end marker');
}
async getFeatures() {
this.send(Message.composeFeatCommand(0));
const replyHeader = await Message.parseRaw(this.receiveMessageHeader());
const reply = await this.receive(replyHeader.bodySize);
return CFLBinaryPListParser.parse(reply);
}
async flashPrimary(payload) {
this.send(Message.composeFlashPrimaryCommand(0, this.password, payload));
const replyHeader = await Message.parseRaw(this.receiveMessageHeader());
return await this.receive(replyHeader.bodySize);
}
}
/**
* Static key/seed for keystream generation
*/
export const ACP_STATIC_KEY = Buffer.from('5b6faf5d9d5b0e1351f2da1de7e8d673', 'hex').toString('utf8');
export function generateACPKeystream(length) {
let key = '';
let key_idx = 0;
while (key_idx < length) {
key += String.fromCharCode((key_idx + 0x55 & 0xFF) ^ ACP_STATIC_KEY.charCodeAt(key_idx % ACP_STATIC_KEY.length));
key_idx++;
}
return key;
}
/**
* ACP message composition and parsing
*/
import { generateACPKeystream } from './keystream';
import struct from 'python-struct';
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
*/
export function generateACPHeaderKey(password) {
const passwordLength = 0x20;
const passwordKey = generateACPKeystream(passwordLength);
const passwordBuffer = password.substr(0, passwordLength).padStart(passwordLength, '\x00');
let encryptedPasswordBuffer = '';
for (let i = 0; i < passwordLength; i++) {
encryptedPasswordBuffer += String.fromCharCode(passwordKey.charCodeAt(i) ^ passwordBuffer.charCodeAt(i));
}
return encryptedPasswordBuffer;
}
export const headerFormat = '!4s8i12x32s48x';
export const headerMagic = 'acpp';
export const headerSize = 128;
export default class Message {
constructor(version, flags, unused, command, errorCode, key, body, bodySize) {
this.version = version;
this.flags = flags;
this.unused = unused;
this.command = command;
this.errorCode = errorCode;
if (typeof body === 'undefined') {
this.bodySize = typeof bodySize !== 'undefined' ? bodySize : -1;
this.bodyChecksum = 1;
} else {
this.bodySize = typeof bodySize !== 'undefined' ? bodySize : body.length;
this.bodyChecksum = adler32.sum(body);
}
this.key = key;
this.body = body;
}
toString() {
let s
= 'ACP message:\n'
+ '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 s;
}
static async parseRaw(data) {
if (headerSize > data.length)
throw new Error(`Need to pass at least ${headerSize} bytes`);
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
});
if (magic !== headerMagic)
throw new Error('Bad header magic');
const versions = [0x00000001, 0x00030001];
if (!versions.includes(version))
throw new Error('Unknown version ' + version);
const tmphdr = struct.pack(headerFormat, [
magic, version,
/* header_checksum: */ 0, body_checksum, body_size,
flags, unused, command, error_code, key
]).toString('binary');
if (header_checksum !== adler32.sum(tmphdr))
throw new Error('Header checksum does not match');
if (body_data && body_size === -1)
throw new Error('Cannot handle stream header with data attached');
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))
throw new Error('Body checksum does not match');
// TODO: check flags
// TODO: check status
const commands = [1, 3, 4, 5, 6, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b];
if (!commands.includes(command))
throw new Error('Unknown command ' + command);
// TODO: check error code
return new Message(version, flags, unused, command, error_code, key, body_data, body_size);
}
static composeEchoCommand(flags, password, payload) {
const message = new Message(0x00030001, flags, 0, 1, 0, generateACPHeaderKey(password), payload);
return message.composeRawPacket();
}
static composeFlashPrimaryCommand(flags, password, payload) {
const message = new Message(0x00030001, flags, 0, 3, 0, generateACPHeaderKey(password), payload);
return message.composeRawPacket();
}
static composeFlashSecondaryCommand(flags, password, payload) {
const message = new Message(0x00030001, flags, 0, 5, 0, generateACPHeaderKey(password), payload);
return message.composeRawPacket();
}
static composeFlashBootloaderCommand(flags, password, payload) {
const message = new Message(0x00030001, flags, 0, 6, 0, generateACPHeaderKey(password), payload);
return message.composeRawPacket();
}
static composeGetPropCommand(flags, password, payload) {
const message = new Message(0x00030001, flags, 0, 0x14, 0, generateACPHeaderKey(password), payload);
return message.composeRawPacket();
}
static composeSetPropCommand(flags, password, payload) {
const message = new Message(0x00030001, flags, 0, 0x15, 0, generateACPHeaderKey(password), payload);
return message.composeRawPacket();
}
static composePerformCommand(flags, password, payload) {
const message = new Message(0x00030001, flags, 0, 0x16, 0, generateACPHeaderKey(password), payload);
return message.composeRawPacket();
}
static composeMonitorCommand(flags, password, payload) {
const message = new Message(0x00030001, flags, 0, 0x18, 0, generateACPHeaderKey(password), payload);
return message.composeRawPacket();
}
static composeRPCCommand(flags, password, payload) {
const message = new Message(0x00030001, flags, 0, 0x19, 0, generateACPHeaderKey(password), payload);
return message.composeRawPacket();
}
static composeAuthCommand(flags, password, payload) {
const message = new Message(0x00030001, flags, 0, 0x1a, 0, generateACPHeaderKey(password), payload);
return message.composeRawPacket();
}
static composeFeatCommand(flags, password, payload) {
const message = new Message(0x00030001, flags, 0, 0x1b, 0, generateACPHeaderKey(password), payload);
return message.composeRawPacket();
}
static composeMessageEx(cls, version, flags, unused, command, errorCode, password, payload, payloadSize) {
const message = new Message(version, flags, unused, command, errorCode, generateACPHeaderKey(password), payload, payloadSize);
return message.composeRawPacket();
}
composeRawPacket() {
let reply = this.composeHeader();
if (this.body)
reply += this.body;
return reply;
}
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');
return header;
}
static get headerFormat() {
return headerFormat;
}
static get headerMagic() {
return headerMagic;
}
static get headerSize() {
return headerSize;
}
}
This diff is collapsed.
import acpProperties from './properties';
const struct = require('python-struct');
export function generateACPProperties() {
const props = [];
for (let prop of acpProperties) {
const [name, type, description, validate] = prop;
if (name.length !== 4) throw new Error('Bad name in ACP properties list: ' + name);
const types = ['str', 'dec', 'hex', 'log', 'mac', 'cfb', 'bin'];
if (!types.includes(type)) throw new Error('Bad type in ACP properties list for name: ' + name + ' - ' + type);
if (!description) throw new Error('Missing description in ACP properties list for name: ' + name);
props.push({ name, type, description, validate });
}
return props;
}
export const props = generateACPProperties();
export const elementHeaderFormat = '!4s2I';
export const elementHeaderSize = struct.sizeOf(elementHeaderFormat);
export default class Property {
constructor(name, value) {
if (name === '\x00\x00\x00\x00' && value === '\x00\x00\x00\x00') {
name = undefined;
value = undefined;
}
if (name && !Property.getSupportedPropertyNames().includes(name))
throw new Error('Invalid property name passed to Property constructor: ' + name);
if (value) {
const propType = this.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);
} 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');
if (validate && !validate(value))
throw new Error('Invalid value passed to validator for property ' + name + ' - type: ' + typeof value);
}
this.name = name;
this.value = value;
}
__init_dec(value) {
if (typeof value === 'number') {
return value;
} else if (typeof value === 'string') {
return parseInt(value);
} else {
throw new Error('Invalid number value: ' + value);
}
}
__init_hex(value) {
if (typeof value === 'number') {
return value;
} else if (typeof value === 'string') {
return value;
} else {
throw new Error('Invalid hex value: ' + value);
}
}
__init_mac(value) {
if (typeof value === 'string') {
if (value.length === 6) return value;
const macBytes = value.split(':');
if (macBytes.length === 6)
return ('').join(macBytes); // unhex
}
throw new Error('Invalid mac value: ' + value);
}
__init_bin(value) {
if (typeof value === 'string') {
return value;
} else {
throw new Error('Invalid bin value: ' + value);
}
}
__init_cfb(value) {
if (typeof value === 'string') {
return value;
} else {
throw new Error('Invalid cfb value: ' + value);
}
}
__init_log(value) {
if (typeof value === 'string') {
return value;
} else {
throw new Error('Invalid log value: ' + value);
}
}
__init_str(value) {
if (typeof value === 'string') {
return value;
} else {
throw new Error('Invalid str value: ' + value);
}
}
toString() {
if (!this.name || !this.value) return '';
const propType = this.getPropertyInfoString(this.name, 'type');
const formatHandlerName = `__format_${propType}`;
// For now just return string value
return this.value.toString();
}
static getSupportedPropertyNames() {
return props.map(prop => prop.name);
}
static getPropertyInfoString(cls, propName, key) {
if (!propName) return;
const prop = props.find(p => p.name === propName);
if (!prop) {
console.error('Property', propName, 'not supported');
return;
}
if (!prop[key]) {
console.error('Invalid property info key', key);
return;
}
return prop[key];
}
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));
}
static async parseRawElementHeader(data) {
return struct.unpack(elementHeaderFormat, Buffer.from(data, 'binary'));
}
static composeRawElement(flags, property) {
const name = property.name ? property.name : '\x00\x00\x00\x00';
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]);
} else if (typeof value === 'string') {
return this.composeRawElementHeader(name, flags, value.length) + value;
} else {
throw new Error('Unhandled property type for raw element composition');
}
}
static composeRawElementHeader(name, flags, size) {
try {
return struct.pack(elementHeaderFormat, [name, flags, size]).toString('binary');
} catch (err) {
console.error('Error packing', name, flags, size, '- :', err);
}
}
}
// import Encryption from './encryption';
import net from 'net';
export default class Session {
constructor(host, port, password) {
this.host = host;
this.port = port;
this.password = password;
this.socket = undefined;
this.buffer = '';
this.reading = 0;
this.encryptionContext = undefined;
this.encryptionMethod = undefined;
this.decryptionMethod = undefined;
}
connect(_timeout = 10000) {
return new Promise((resolve, reject) => {
this.socket = new net.Socket();
const timeout = setTimeout(() => {
this.reading -= 1;
reject('Timeout');
}, _timeout);
this.socket.connect(this.port, this.host, err => {
console.log('Connected', err);