...
 
Commits (9)
#!/usr/bin/env node
require('../dist/cli');
This diff is collapsed.
......@@ -12,11 +12,16 @@
"url": "https://gitlab.fancy.org.uk/samuel/node-acp"
},
"main": "dist/client.js",
"bin": {
"acp": "bin/acp.js"
},
"files": [
"dist"
],
"dependencies": {
"adler32": "^0.1.7"
"adler32": "^0.1.7",
"bonjour": "^3.5.0",
"yargs": "^12.0.2"
},
"devDependencies": {
"babel-core": "^6.26.3",
......
This diff is collapsed.
#!/usr/bin/env node
import Client from './client';
import Server from './server';
import Property from './property';
import yargs from 'yargs';
import bonjour from 'bonjour';
yargs.demandCommand();
yargs.help();
yargs.option('host', {
alias: 'h',
describe: 'The hostname/IP address of the AirPort device to connect to',
});
yargs.option('port', {
describe: 'The port of the AirPort device\'s acp daemon (you should only need to change this when you need to use port forwarding to access the device)',
default: 5009,
});
yargs.option('password', {
alias: 'p',
describe: 'The device password of the AirPort device',
});
yargs.command('version', 'Shows the node-acp version', () => {}, argv => {
console.log('node-acp v' + require('../package').version);
console.log('https://gitlab.fancy.org.uk/samuel/node-acp');
});
yargs.command('server', 'Start the ACP server', yargs => {
yargs.option('advertise', {
describe: 'Whether to advertise the ACP server with DNS SD',
boolean: true,
});
yargs.option('advertise-name', {
describe: 'The name to advertise the service as',
default: 'node-acp',
});
yargs.option('advertise-network', {
describe: 'The network name to advertise',
default: 'node-acp',
});
yargs.option('advertise-address', {
describe: 'The MAC address to advertise',
default: '00-00-00-00-00-00',
});
}, async argv => {
const server = new Server(argv.host || '::', argv.port, argv.password);
try {
await server.listen();
} catch (err) {
console.error(err);
}
// Leave the server to run
if (argv.advertise) {
const service = bonjour().publish({
name: argv['advertise-name'],
port: argv.port,
type: 'airport',
txt: {waMA: '00-00-00-00-00-00,' + Object.entries({
// waMA: '00-00-00-00-00-00', // Ethernet MAC address
raMA: argv['advertise-address'], // 5 GHz Wi-Fi MAC address - this is used to identify devices in AirPort Utility
raM2: '00-00-00-00-00-00', // 2.4 GHz Wi-Fi MAC address
raNm: argv['advertise-network'], // Network
raCh: 1, // 2.4 GHz channel
rCh2: 36, // 5 GHz channel
raSt: 0, // ?
raNA: 0, // ?
syFl: '0x820C', // ?
syAP: 115, // Model?
syVs: '7.8', // Version
srcv: '78000.12', // Build
bjSd: 43, // ?
// prob: '',
}).map(([k, v]) => `${k}=${v}`).join(',')},
});
console.log('Advertising service', service);
}
});
const commandHandler = handler => async argv => {
const client = new Client(argv.host || 'airport-base-station.local', argv.port, argv.password);
try {
await client.connect();
await handler.call(undefined, client, argv);
} catch (err) {
console.error(err);
}
await client.disconnect();
process.exit();
};
yargs.command('authenticate', 'Authenticate', yargs => {}, commandHandler(async (client, argv) => {
const data = await client.authenticate();
console.log(data);
}));
yargs.command('getprop <prop>', 'Get an ACP property', yargs => {
yargs.positional('prop', {
describe: 'The name of the ACP property',
});
}, commandHandler(async (client, argv) => {
const props = await client.getProperties([argv.prop]);
console.log(props[0].format());
}));
yargs.command('setprop <prop> <value>', 'Set an ACP property', yargs => {
yargs.positional('prop', {
describe: 'The name of the ACP property',
}).positional('value', {
describe: 'The new value',
});
}, commandHandler(async (client, argv) => {
const props = await client.setProperties([new Property(argv.prop, argv.value)]);
console.log(props);
}));
const argv = yargs.argv;
import Session from './session';
import Message from './message';
import Property, {elementHeaderSize} from './property';
// import { CFLBinaryPListParser } from './cflbinary';
import Message, {HEADER_SIZE as MESSAGE_HEADER_SIZE} from './message';
import Property, {HEADER_SIZE as ELEMENT_HEADER_SIZE} from './property';
import CFLBinaryPList from './cflbinary';
export default class Client {
constructor(host, port, password) {
......@@ -30,57 +30,61 @@ export default class Client {
}
receiveMessageHeader() {
return this.receive(Message.headerSize);
return this.receive(MESSAGE_HEADER_SIZE);
}
receivePropertyElementHeader() {
return this.receive(elementHeaderSize);
return this.receive(ELEMENT_HEADER_SIZE);
}
/**
* Gets properties from the AirPort device.
*
* Client: GetProp {...Property}
* Server: GetProp
* Server: ...Property
*/
async getProperties(prop_names) {
let payload = '';
for (let name of prop_names) {
payload += Property.composeRawElement(0, new Property(name));
payload += Property.composeRawElement(0, name instanceof Property ? name : 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,
});
const reply_header = await Message.parseRaw(reply);
if (replyHeader.errorCode !== 0) {
console.log('Client.getProperties error code:', replyHeader.errorCode);
return [];
if (reply_header.error_code !== 0) {
throw new Error('Error ' . reply_header.error_code);
}
const props = [];
while (true) {
const propHeader = await this.receivePropertyElementHeader();
console.debug('Received property element header:', propHeader);
const {name, flags, size} = await Property.parseRawElementHeader(propHeader);
const prop_header = await this.receivePropertyElementHeader();
console.debug('Received property element header:', prop_header);
const data = await Property.parseRawElementHeader(prop_header);
console.debug(data);
const {name, flags, size} = data;
const propData = await this.receive(size);
const value = await this.receive(size);
if (flags & 1) {
const errorCode = Buffer.from(propData, 'binary').readInt32BE(0);
console.log('error requesting value for property', name, '-', errorCode);
continue;
const error_code = Buffer.from(value, 'binary').readInt32BE(0);
throw new Error('Error requesting value for property "' + name + '": ' + error_code);
}
const prop = new Property(name, propData);
console.debug('prop', prop);
const prop = new Property(name, value);
if (typeof prop.name === 'undefined' && typeof prop.value === 'undefined') {
console.debug('found empty prop end marker');
break;
}
console.debug('Prop', prop);
props.push(prop);
}
......@@ -89,35 +93,34 @@ export default class Client {
async setProperties(props) {
let payload = '';
for (let {prop} of props) {
console.debug('prop', prop);
for (let prop of props) {
payload += Property.composeRawElement(0, prop);
}
const request = Message.composeSetPropCommand(0, this.password, payload);
this.send(request);
await this.send(request);
const rawReply = await this.receiveMessageHeader();
const replyHeader = await Message.parseRaw(rawReply);
const raw_reply = await this.receiveMessageHeader();
const reply_header = await Message.parseRaw(raw_reply);
if (replyHeader.errorCode !== 0) {
console.log('set properties error code', replyHeader.errorCode);
if (reply_header.error_code !== 0) {
console.log('set properties error code', reply_header.error_code);
return;
}
const propHeader = await this.receivePropertyElementHeader();
const {name, flags, size} = await Property.parseRawElementHeader(propHeader);
const prop_header = await this.receivePropertyElementHeader();
const {name, flags, size} = await Property.parseRawElementHeader(prop_header);
const propData = await this.receive(size);
const value = await this.receive(size);
if (flags) {
const errorCode = Buffer.from(propData, 'binary').readUInt32BE(0);
console.log('error setting value for property', name, '-', errorCode);
return;
if (flags & 1) {
const error_code = Buffer.from(value, 'binary').readUInt32BE(0);
throw new Error('Error setting value for property "' + name + '": ' + error_code);
}
const prop = new Property(name, propData);
console.debug('prop', prop);
const prop = new Property(name, value);
console.debug('Prop', prop);
if (typeof prop.name === 'undefined' && typeof prop.value === 'undefined') {
console.debug('found empty prop end marker');
......@@ -125,15 +128,35 @@ export default class Client {
}
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);
await this.send(Message.composeFeatCommand(0));
const reply_header = await Message.parseRaw(await this.receiveMessageHeader());
const reply = await this.receive(reply_header.body_size);
return CFLBinaryPList.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);
const reply_header = await Message.parseRaw(this.receiveMessageHeader());
return await this.receive(reply_header.body_size);
}
async authenticate() {
let payload = {
state: 1,
username: 'admin',
};
const message = Message.composeAuthCommand(4, this.password, CFLBinaryPList.compose(payload));
await this.send(message);
const response = await this.session.receiveMessage();
const data = CFLBinaryPList.parse(response.body);
if (response.error_code !== 0) {
console.log('Authenticate error code', response.error_code);
return;
}
return data;
}
}
......@@ -7,15 +7,15 @@ export const ACP_STATIC_KEY = Buffer.from('5b6faf5d9d5b0e1351f2da1de7e8d673', 'h
export function generateACPKeystream(length) {
let key = '';
let key_idx = 0;
let idx = 0;
while (key_idx < length) {
while (idx < length) {
key += String.fromCharCode(
(key_idx + 0x55 & 0xFF) ^
ACP_STATIC_KEY.charCodeAt(key_idx % ACP_STATIC_KEY.length)
(idx + 0x55 & 0xFF) ^
ACP_STATIC_KEY.charCodeAt(idx % ACP_STATIC_KEY.length)
);
key_idx++;
idx++;
}
return key;
......
This diff is collapsed.
import acpProperties from './properties';
import CFLBinaryPList from './cflbinary';
import acp_properties from './properties';
export function generateACPProperties() {
const props = [];
for (let prop of acpProperties) {
const [name, type, description, validate] = prop;
for (let prop of acp_properties) {
const [name, type, description, validator] = prop;
if (name.length !== 4) throw new Error('Bad name in ACP properties list: ' + name);
......@@ -14,7 +15,7 @@ export function generateACPProperties() {
if (!description) throw new Error('Missing description in ACP properties list for name: ' + name);
props.push({name, type, description, validate});
props.push({name, type, description, validator});
}
return props;
......@@ -22,7 +23,7 @@ export function generateACPProperties() {
export const props = generateACPProperties();
export const elementHeaderSize = 12;
export const HEADER_SIZE = 12;
export default class Property {
constructor(name, value) {
......@@ -36,19 +37,15 @@ export default class Property {
}
if (value) {
const propType = this.constructor.getPropertyInfoString(name, 'type');
const initHandlerName = `__init_${propType}`;
const prop_type = this.constructor.getPropertyInfoString(name, 'type');
const init_handler_name = `__init_${prop_type}`;
if (!this[initHandlerName]) throw new Error(`Missing handler for ${propType} property type`);
if (!this[init_handler_name]) throw new Error(`Missing handler for ${prop_type} property type`);
try {
value = this[initHandlerName](value);
} catch (err) {
throw new Error(JSON.stringify(err, null, 4) + ' provided for ' + propType + ' property type ' + value);
}
value = this[init_handler_name](value);
const validate = this.constructor.getPropertyInfoString(name, 'validate');
if (validate && !validate(value)) {
const validator = this.constructor.getPropertyInfoString(name, 'validator');
if (validator && !validator(value, name)) {
throw new Error('Invalid value passed to validator for property ' + name + ' - type: ' + typeof value);
}
}
......@@ -61,7 +58,7 @@ export default class Property {
if (typeof value === 'number') {
return value;
} else if (typeof value === 'string') {
return Buffer.from(value, 'binary').readUInt32BE(0);
return Buffer.from(value, 'binary').readUIntBE(0, value.length);
} else {
throw new Error('Invalid number value: ' + value);
}
......@@ -144,24 +141,29 @@ export default class Property {
__format_mac(value) {
const mac_bytes = [];
value = Buffer.from(value, 'binary').toString('hex');
for (let i = 0; i < 6; i++) {
mac_bytes.push(value.substr(i, 1));
mac_bytes.push(value.substr(i, 2));
}
return implode(':', mac_bytes);
return mac_bytes.join(':');
}
__format_bin(value) {
return value.toString();
return Buffer.from(value, 'binary').toString('hex');
}
__format_cfb(value) {
return value.toString();
return CFLBinaryPList.parse(value);
}
__format_log(value) {
return value.toString();
return value.split('\x00').map(line => line.trim() + '\n').join('');
}
__format_str(value) {
return Buffer.from(value, 'binary').toString('utf8');
}
toString() {
......@@ -192,9 +194,9 @@ export default class Property {
static async parseRawElement(data) {
// eslint-disable-next-line no-unused-vars
const {name, flags, size} = await this.parseRawElementHeader(data.substr(0, elementHeaderSize));
const {name, flags, size} = await this.parseRawElementHeader(data.substr(0, HEADER_SIZE));
// TODO: handle flags
return new this(name, data.substr(elementHeaderSize));
return new this(name, data.substr(HEADER_SIZE));
}
static async parseRawElementHeader(data) {
......@@ -238,7 +240,7 @@ export default class Property {
}
static unpackHeader(header_data) {
if (header_data.length !== elementHeaderSize) {
if (header_data.length !== HEADER_SIZE) {
throw new Error('Header data must be 12 characters');
}
......
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 net from 'net';
export default class Server {
constructor(host, port, password) {
this.host = host;
this.port = port;
this.password = password;
this.socket = undefined;
}
listen(_timeout) {
return new Promise((resolve, reject) => {
this.socket = new net.Server();
setTimeout(() => {
this.reading -= 1;
reject('Timeout');
}, _timeout);
this.socket.listen(this.port, this.host, err => {
console.log('Connected', err);
if (err) reject(err);
else resolve();
});
this.socket.on('close', had_error => {
this.socket = undefined;
});
this.socket.on('connection', connection => {
this.handleConnection(connection);
});
});
}
close() {
if (!this.socket) return;
this.socket.end();
return new Promise((resolve, reject) => {
this.socket.on('close', resolve);
});
}
handleConnection(socket) {
const session = new Session(socket.remoteAddress, socket.remotePort, this.password);
session.socket = socket;
console.log('New connection from', session.host, session.port);
socket.on('data', data => {
console.debug(0, 'Receiving data', data, 'on connection', session.host + ' port ' + session.port);
if (session.reading) return;
session.buffer += data.toString('binary');
// Try decoding the data as a message
this.handleData(session);
});
}
async handleData(session) {
while (session.buffer.length >= MESSAGE_HEADER_SIZE) {
await this.tryHandleMessage(session);
}
}
async tryHandleMessage(session) {
try {
const [message, data] = await Message.parseRaw(session.buffer, true);
session.buffer = data;
if (message.body.length !== message.body_size) {
// Haven't received the message body yet
return;
}
// session.buffer = session.buffer.substr(MESSAGE_HEADER_SIZE);
this.handleMessage(session, message);
} catch (err) {
console.error('Error handling message from', session.host, session.port, err);
}
}
async handleMessage(session, message) {
console.log('Received message', message);
switch (message.command) {
// 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;
const value = data.substr(0, ELEMENT_HEADER_SIZE + size);
data = data.substr(ELEMENT_HEADER_SIZE + size);
const prop = new Property(name, value);
if (typeof prop.name === 'undefined' && typeof prop.value === 'undefined') {
break;
}
props.push(prop);
}
// Send back an array of Propertys
const ret = this.getProperties(props);
let payload = '', i = 0;
for (let prop of ret) {
payload += Property.composeRawElement(0, prop instanceof Property ? prop : new Property(props[i], prop));
i++;
}
const response = Message.composeGetPropCommand(4, this.password, payload);
return;
}
}
}
getProperties(props) {
return props.map(prop => this.getProperty(prop));
}
getProperty(prop) {
if (prop.name === 'dbug') return new Property('dbug', 0x3000);
}
}
// import Encryption from './encryption';
import Message, {HEADER_SIZE as MESSAGE_HEADER_SIZE} from './message';
import Property, {HEADER_SIZE as ELEMENT_HEADER_SIZE} from './property';
// import {ClientEncryption, ServerEncryption} from './encryption';
import net from 'net';
export default class Session {
......@@ -12,9 +15,7 @@ export default class Session {
this.buffer = '';
this.reading = 0;
this.encryptionContext = undefined;
this.encryptionMethod = undefined;
this.decryptionMethod = undefined;
this.encryption = undefined;
}
connect(_timeout = 10000) {
......@@ -48,30 +49,48 @@ export default class Session {
if (!this.socket) return;
this.socket.end();
return new Promise((resolve, reject) => {
this.socket.on('close', resolve);
});
}
async sendAndReceive(data, size, timeout = 10000) {
await this.send(data);
async receiveMessage(timeout) {
const raw_header = await this.receiveMessageHeader(timeout);
const message = await Message.parseRaw(raw_header);
data = await this.receiveSize(size, timeout);
const data = await this.receive(message.body_size);
if (this.decryptionMethod) {
data = this.decryptionMethod(data);
}
message.body = data;
return data;
return message;
}
receiveMessageHeader(timeout) {
return this.receive(MESSAGE_HEADER_SIZE, timeout);
}
receivePropertyElementHeader(timeout) {
return this.receive(ELEMENT_HEADER_SIZE, timeout);
}
async sendAndReceive(data, size, timeout = 10000) {
await this.send(data);
return await this.receive(size, timeout);
}
send(data) {
if (data instanceof Message) {
data = data.composeRawPacket();
}
if (!Buffer.isBuffer(data)) {
data = Buffer.from(data, 'binary');
}
if (this.encryptionMethod) {
data = this.encryptionMethod(data);
if (this.encryption) {
data = this.encryption.encrypt(data);
}
if (!this.socket) return;
......@@ -143,28 +162,18 @@ export default class Session {
async receive(size, timeout = 10000) {
let data = await this.receiveSize(size, timeout);
if (this.decryptionMethod) {
data = this.decryptionMethod(data);
if (this.encryption) {
data = this.encryption.decrypt(data);
}
return data;
}
}
export class ClientSession extends Session {
enableEncryption(key, client_iv, server_iv) {
this.encryptionContext = new Encryption(key, client_iv, server_iv);
this.encryptionMethod = this.encryptionContext.clientEncrypt;
this.decryptionMethod = this.encryptionContext.serverDecrypt;
this.encryption_context = new ClientEncryption(key, client_iv, server_iv);
}
}
export class ServerSession extends Session {
enableEncryption(key, client_iv, server_iv) {
this.encryptionContext = new Encryption(key, client_iv, server_iv);
this.encryptionMethod = this.encryptionContext.serverEncrypt;
this.decryptionMethod = this.encryptionContext.clientDecrypt;
enableServerEncryption(key, client_iv, server_iv) {
this.encryption_context = new ServerEncryption(key, client_iv, server_iv);
}
}
const {default: CFLBinaryPList} = require('../dist/cflbinary');
const plist = Buffer.from('43464230d07070726f626c656d7300a00000454e4421', 'hex');
const object = CFLBinaryPList.parse(plist.toString('binary'));
// Should log {problems: []}
console.log(object);
const object2 = {something: {lol: ['i', 'd', 'k']},
brillant: [1, 2, 3, 4, 5, 0x10000000]};
const plist2 = CFLBinaryPList.compose(object2);
console.log(object2, plist2);
const object2_2 = CFLBinaryPList.parse(plist2);
// Should log object2
console.log(object2_2);
const object3 = [1, 2, 3, 4, 65535, 65536, 65537];
const plist3 = CFLBinaryPList.compose(object3);
console.log(object3, plist3);
const object3_2 = CFLBinaryPList.parse(plist3);
// Should log object3
console.log(object3_2);
const object4 = [1.1, 65534.1, 65534.1];
const plist4 = CFLBinaryPList.compose(object4);
console.log(object4, plist4);
const object4_2 = CFLBinaryPList.parse(plist4);
// Should log object4
console.log(object4_2);
const {default: Client} = require('..');
const client = new Client('192.168.2.251', 5009, 'testing');
(async () => {
await client.connect();
console.log('Connected to', client.host, client.port);
const features = await client.getFeatures();
console.log('Features', features);
process.exit();
})();