Skip to content

Commit

Permalink
Add APIs for network interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
keeramis committed Jan 26, 2024
1 parent df444d8 commit b5d74f9
Show file tree
Hide file tree
Showing 7 changed files with 323 additions and 11 deletions.
44 changes: 44 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
},
"dependencies": {
"@particle/device-os-protobuf": "^2.2.0",
"ip-address": "^9.0.5",
"protobufjs": "^6.11.3",
"usb": "^2.11.0"
},
Expand Down
8 changes: 8 additions & 0 deletions src/address-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
function convertBufferToMacAddress(buffer) {
const bytes = Array.from(buffer);
return bytes.map(byte => byte.toString(16).padStart(2, '0')).join(':');
}

module.exports = {
convertBufferToMacAddress
};
11 changes: 11 additions & 0 deletions src/address-util.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const addressUtil = require('./address-util');
const { expect } = require('../test/support');

describe('convertBufferToMacAddress', () => {
it('should convert a buffer to MAC address', () => {
const buffer = Buffer.from([0x00, 0x11, 0x22, 0x33, 0x44, 0x55]); // Buffer with MAC address bytes
const expected = '00:11:22:33:44:55';
const result = addressUtil.convertBufferToMacAddress(buffer);
expect(result).to.eql(expected);
});
});
127 changes: 126 additions & 1 deletion src/network-device.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
const { Request } = require('./request');
const { fromProtobufEnum } = require('./protobuf-util');

const { convertBufferToMacAddress } = require('./address-util.js');
const { globalOptions } = require('./config');
const proto = require('./protocol');
const { Address4, Address6 } = require('ip-address');
const { NotFoundError } = require('./error.js');

const DEFAULT_INTERFACE = 1;

Expand All @@ -13,6 +16,59 @@ const NetworkStatus = fromProtobufEnum(proto.NetworkState, {
UP: 'UP'
});

const InterfaceType = fromProtobufEnum(proto.InterfaceType, {
INVALID : 'INVALID_INTERFACE_TYPE',
LOOPBACK : 'LOOPBACK',
THREAD : 'THREAD',
ETHERNET : 'ETHERNET',
WIFI : 'WIFI',
PPP : 'PPP'
});

const InterfaceConfigurationSource = fromProtobufEnum(proto.InterfaceConfigurationSource, {
NONE: 'NONE',
DHCP: 'DHCP',
STATIC: 'STATIC',
SLAAC: 'SLAAC',
DHCPV6: 'DHCPV6'
});

/**
* Converts a given interface IP address object with version into a dotted-decimal format.
*
* @param {object} ifaceAddr
* @param {string} version 'v4' or 'v6'
* @returns {string} address in dotted-decimal format
*/
function convertInterfaceAddress(ifaceAddr, version) {
let res = null;
let prefixLength = null;
if (ifaceAddr && ifaceAddr.address && ifaceAddr.address[version]) {
const addrObj = ifaceAddr.address[version];
res = convertIPAddress(addrObj, version);
}
if (ifaceAddr && ifaceAddr.prefixLength) {
prefixLength = ifaceAddr.prefixLength;
}
return res ? `${res}/${prefixLength}` : null;
}

/**
* Converts a given IP address object with version into a dotted-decimal address string.
*
* @param {object} addr addr.address sent as int32 for 'v4' and as a buffer for 'v6'
* @param {string} version 'v4' or 'v6'
* @returns {string} address in dotted-decimal format
*/
function convertIPAddress(addr, version) {
if (addr && addr.address) {
const val = addr.address;
const res = version === 'v4' ? Address4.fromInteger(val) : Address6.fromByteArray(val);
return res.address;
}
return null;
}

/**
* Network device.
*
Expand Down Expand Up @@ -71,6 +127,75 @@ const NetworkDevice = base => class extends base {
setNetworkConfig(config) {
return this.sendRequest(Request.NETWORK_SET_CONFIGURATION, config); // TODO
}

/**
* Gets the list of network interfaces
*
* Supported platforms:
* - Gen 3 (since Device OS 0.9.0)
* - Gen 2 (since Device OS 0.8.0)
*
* @param {Object} [options] Options.
* @param {Number} [options.timeout] Timeout (milliseconds).
* @return {Promise<Object>}
*/
async getNetworkInterfaceList({ timeout = globalOptions.requestTimeout } = {}) {
const res = await this.sendRequest(Request.NETWORK_GET_INTERFACE_LIST, null, { timeout });
return res.interfaces.map(entry => ({
index: entry.index,
name: entry.name,
type: InterfaceType.fromProtobuf(entry.type)
}));
}

/**
* Gets the network interface and its fields
*
* Supported platforms:
* - Gen 3 (since Device OS 0.9.0)
* - Gen 2 (since Device OS 0.8.0)
*
* @param {Object} [options] Options.
* @param {Number} [options.timeout] Timeout (milliseconds).
* @return {Promise<Object>}
*/
async getNetworkInterface({ index, timeout = globalOptions.requestTimeout } = {}) {
const reply = await this.sendRequest(Request.NETWORK_GET_INTERFACE, { index, timeout });

if (!reply.interface) {
throw new NotFoundError();
}

const { index: ifaceIndex, name, type, ipv4Config, ipv6Config, hwAddress } = reply.interface;

const result = {
index: ifaceIndex,
name,
type: InterfaceType.fromProtobuf(type),
hwAddress: convertBufferToMacAddress(hwAddress)
};

if (ipv4Config) {
result.ipv4Config = {
addresses: ipv4Config.addresses.map((addr) => convertInterfaceAddress(addr, 'v4')),
gateway: convertIPAddress(ipv4Config.gateway, 'v4'),
peer: convertIPAddress(ipv4Config.peer, 'v4'),
dns: ipv4Config.dns.map((addr) => convertIPAddress(addr, 'v4')),
source: InterfaceConfigurationSource.fromProtobuf(ipv4Config.source)
};
}

if (ipv6Config) {
result.ipv6Config = {
addresses: ipv6Config.addresses.map((addr) => convertInterfaceAddress(addr, 'v6')),
gateway: convertIPAddress(ipv6Config.gateway, 'v6'),
dns: ipv6Config.dns.map((addr) => convertIPAddress(addr, 'v6')),
source: InterfaceConfigurationSource.fromProtobuf(ipv6Config.source)
};
}

return result;
}
};

module.exports = {
Expand Down
133 changes: 123 additions & 10 deletions src/network-device.test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,125 @@
const { expect } = require('../test/support');
const { setDevicePrototype } = require('./set-device-prototype');
describe('NetworkDevice (all deprecated methods)', () => {
const deprecatedMethods = ['getNetworkStatus', 'getNetworkConfig', 'setNetworkConfig'];
for (const method of deprecatedMethods) {
it(`provides ${method}`, async () => {
const fakeDevice = { type: 'p2' }; // could be any device type
const result = setDevicePrototype(fakeDevice);
expect(result[method]).to.be.a('Function');
});
}
const { fakeUsb, sinon, expect } = require('../test/support');
const proxyquire = require('proxyquire');
const { NotFoundError } = require('./error');

const { getDevices } = proxyquire('../src/device-base', {
'./usb-device-node': fakeUsb
});

describe('NetworkDevice', () => {
beforeEach(async () => {
const usbDevs = [];fakeUsb.addDevice({ vendorId: 0xaaaa, productId: 0xaaaa });
usbDevs.push(fakeUsb.addP2());
});

afterEach(() => {
sinon.restore();
});

describe('NetworkDevice (all deprecated methods)', () => {

const deprecatedMethods = ['getNetworkStatus', 'getNetworkConfig', 'setNetworkConfig'];
for (const method of deprecatedMethods) {
it(`provides ${method}`, async () => {
const fakeDevice = { type: 'p2' }; // could be any device type
const result = setDevicePrototype(fakeDevice);
expect(result[method]).to.be.a('Function');
});
}
});

describe('getNetworkInfo', () => {
let dev;
beforeEach(async () => {
const devs = await getDevices();
dev = setDevicePrototype(devs[0]);
await dev.open();
});

afterEach(async () => {
await dev.close();
});

it('parses the result', async () => {
const input = {
'index': 4,
'name': 'wl3',
'type': 8,
'ipv4Config': {
'dns': [
{
'address': 3232257567
}
],
'addresses': [
{
'address': {
'v4': {
'address': 3232257567
}
},
'prefixLength': 24
}
]
},
'ipv6Config': {
'dns': [
{
'address': Buffer.from([2, 1, 4, 3, 6, 5, 8, 7, 10, 9, 12, 11, 14, 13, 15])
}
],
'addresses': [
{
'address': {
'v6': {
'address': Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])
}
},
'prefixLength': 24
}
]
},
'hwAddress': Buffer.from([48,174,164,229,83,16])
};

const expectedOutput = {
'index': 4,
'name': 'wl3',
'type': 'WIFI',
'hwAddress': '30:ae:a4:e5:53:10',
'ipv4Config': {
'addresses': ['192.168.86.31/24'],
'gateway': null,
'peer': null,
'dns': ['192.168.86.31'],
'source': 'UNKNOWN'
},
'ipv6Config': {
'addresses': ['0001:0203:0405:0607:0809:0a0b:0c0d:0e0f/24'],
'gateway': null,
'dns': ['0002:0104:0306:0508:070a:090c:0b0e:0d0f'],
'source': 'UNKNOWN'
}
};
sinon.stub(dev, 'sendRequest').resolves({ interface: input });

const res = await dev.getNetworkInterface({ index: 4 });

expect(res).to.eql(expectedOutput);
});

it('returns an error if the interface is invalid', async () => {
sinon.stub(dev, 'sendRequest').resolves({});

let error;
try {
await dev.getNetworkInterface({ index: 4 });
} catch (_e) {
error = _e;
}

expect(error).to.be.an.instanceOf(NotFoundError);
});
});
});
Loading

0 comments on commit b5d74f9

Please sign in to comment.