diff --git a/README.md b/README.md index ee782d2..4ad7a8b 100644 --- a/README.md +++ b/README.md @@ -4,5 +4,23 @@ not sure how far I will take this but messing around with E1.17 ACN protocol ## areas - rlp - sdt + - vectors + - [x] JOIN + - [x] JOIN_REFUSE + - [x] JOIN_ACCEPT + - [x] LEAVING + - [x] NAK + - [x] REL_WRAP + - [x] UNREL_WRAP + - [x] GET_SESSIONS + - [ ] SESSIONS + - [x] ACK + - [ ] CHANNEL_PARAMS + - [ ] LEAVE + - [x] CONNECT + - [x] CONNECT_ACCEPT + - [x] CONNECT_REFUSE + - [x] DISCONNECT + - [x] DISCONNECTING - dmp - ddl diff --git a/packages/acn/src/enums.ts b/packages/acn/src/enums.ts new file mode 100644 index 0000000..584f966 --- /dev/null +++ b/packages/acn/src/enums.ts @@ -0,0 +1,45 @@ +export enum Protocol { + SDT = 1, +} + +export enum SDTVector { + REL_WRAP = 1, + UNREL_WRAP, + CHANNEL_PARAMS, + JOIN, + JOIN_REFUSE, + JOIN_ACCEPT, + LEAVE, + LEAVING, + CONNECT, + CONNECT_ACCEPT, + CONNECT_REFUSE, + DISCONNECT, + DISCONNECTING, + ACK, + NAK, + GET_SESSIONS, + SESSIONS, +} + +export enum TransportLayerAddressType { + SDT_ADDR_NULL = 0, + SDT_ADDR_IPV4, + SDT_ADDR_IPV6, +} + +export enum SDTReasonCode { + NONSPECIFIC = 1, + ILLEGAL_PARAMETERS, + LOW_RESOURCES, + ALREADY_MEMBER, + BAD_ADDRESS_TYPE, + NO_RECIPROCAL_CHANNEL, + CHANNEL_EXPIRED, + LOST_SEQUENCE, + SATURATED, + TRANSPORT_ADDRESS_CHANGING, + ASKED_TO_LEAVE, + NO_RECIPIENT, + ONLY_UNICAST_SUPPORTED, +} diff --git a/packages/acn/src/index.ts b/packages/acn/src/index.ts index 6d508cd..a64b562 100644 --- a/packages/acn/src/index.ts +++ b/packages/acn/src/index.ts @@ -1,2 +1,2 @@ -export * from './models'; +export * from './types'; export * from './udp'; diff --git a/packages/acn/src/models.ts b/packages/acn/src/models.ts deleted file mode 100644 index a339a25..0000000 --- a/packages/acn/src/models.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type Protocols = { - SDT: 1; -}; - -export type UDPPreamble = { - preambleSize: number; - postambleSize: number; - packetIdentifier: string; -}; - -export type ACNPacket = { - preamble: PreambleType; - pduBlock: PDUBlockType; - postamble: PostambleType; -}; - -export type RootLayerPDU = { - vector: number; - header: string; - data: Uint8Array; -}; diff --git a/packages/acn/src/pdu/index.ts b/packages/acn/src/pdu/index.ts index 58670a5..07a880b 100644 --- a/packages/acn/src/pdu/index.ts +++ b/packages/acn/src/pdu/index.ts @@ -1,4 +1,6 @@ import rlp from './rlp'; +import sdt from './sdt'; export default { rlp, + sdt, }; diff --git a/packages/acn/src/pdu/rlp.ts b/packages/acn/src/pdu/rlp.ts index bf8e9dc..4c3d132 100644 --- a/packages/acn/src/pdu/rlp.ts +++ b/packages/acn/src/pdu/rlp.ts @@ -1,4 +1,6 @@ -import { RootLayerPDU } from '../models'; +import pdu from '.'; +import { Protocol } from '../enums'; +import { RootLayerPDU, SessionDataTransportPDU } from '../types'; import { toHex } from '../utils'; function decode(bytes: Uint8Array): RootLayerPDU { @@ -49,7 +51,22 @@ function decode(bytes: Uint8Array): RootLayerPDU { const dataOffset = headerOffset + 16; // NOTE(jwetzell): flags/lengthH + lengthL + lengthX + vector + header const dataLength = length - (1 + 1 + (lengthFlag ? 1 : 0) + 4 + 16); - const data = bytes.subarray(dataOffset, dataOffset + dataLength); + let data: SessionDataTransportPDU[] | Uint8Array = bytes.subarray(dataOffset, dataOffset + dataLength); + + if (vector === Protocol.SDT) { + const blocks: SessionDataTransportPDU[] = []; + + let blockDataOffset = 0; + // TODO(jwetzell): this could blow up so might need some attention later? + while (blockDataOffset < dataLength) { + const blockData = pdu.sdt.decode(bytes.subarray(dataOffset + blockDataOffset)); + blocks.push(blockData); + blockDataOffset += blockData.length; + } + data = blocks; + } else { + console.error(`unhandled protocol in RLP: ${vector}`); + } return { vector, diff --git a/packages/acn/src/pdu/sdt.ts b/packages/acn/src/pdu/sdt.ts new file mode 100644 index 0000000..e87cf7e --- /dev/null +++ b/packages/acn/src/pdu/sdt.ts @@ -0,0 +1,275 @@ +import { Protocol, SDTVector } from '../enums'; +import { + SDTAckData, + SDTConnectAcceptData, + SDTConnectData, + SDTConnectRefuseData, + SDTDisconnectData, + SDTDisconnectingData, + SDTGetSessionsData, + SDTJoinAcceptData, + SDTJoinData, + SDTJoinRefuseData, + SDTLeavingData, + SDTNakData, + SDTWrapperData, + SessionDataTransportPDU, + TransportLayerAddress, +} from '../types'; +import { toHex } from '../utils'; + +// TODO(jwetzell): work out flag inheritance, will need previous PDU + +export function decode(bytes: Uint8Array): SessionDataTransportPDU { + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const flags = view.getUint8(0) >> 4; + const lengthFlag = ((flags >> 3) & 0x1) === 1; + const vectorFlag = ((flags >> 2) & 0x1) === 1; + + if (!vectorFlag) { + throw new Error('SDT PDU must have a vector'); + } + + const dataFlag = (flags & 0x1) === 1; + + if (!dataFlag) { + // TODO(jwetzell): idk if this is true + throw new Error('SDT PDU must have data'); + } + + const lengthH = view.getUint8(0) & 0x0f; + let lengthOffset = 1; + const lengthL = view.getUint8(lengthOffset); + + let length = (lengthH << 8) + lengthL; + if (lengthFlag) { + lengthOffset += 1; + const lengthX = view.getUint8(lengthOffset); + length = (lengthH << 16) + (lengthL << 8) + lengthX; + } + let vectorOffset = lengthOffset + 1; + + if (lengthFlag) { + vectorOffset += 1; + } + + const vector = view.getUint8(vectorOffset); + + const dataOffset = vectorOffset + 1; + // NOTE(jwetzell): flags/lengthH + lengthL + lengthX + vector + const dataLength = length - (1 + 1 + (lengthFlag ? 1 : 0) + 1); + const data = decodeData(vector, bytes.subarray(dataOffset, dataOffset + dataLength)); + + return { + vector, + length, + data, + }; +} + +function decodeClientBlock(bytes: Uint8Array) { + if (bytes.byteLength === 0) { + return undefined; + } + + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const flags = view.getUint8(0) >> 4; + const lengthFlag = ((flags >> 3) & 0x1) === 1; + const vectorFlag = ((flags >> 2) & 0x1) === 1; + + if (!vectorFlag) { + throw new Error('SDT PDU must have a vector'); + } + + const dataFlag = (flags & 0x1) === 1; + + if (!dataFlag) { + // TODO(jwetzell): idk if this is true + throw new Error('SDT PDU must have data'); + } + + const lengthH = view.getUint8(0) & 0x0f; + let lengthOffset = 1; + const lengthL = view.getUint8(lengthOffset); + + let length = (lengthH << 8) + lengthL; + if (lengthFlag) { + lengthOffset += 1; + const lengthX = view.getUint8(lengthOffset); + length = (lengthH << 16) + (lengthL << 8) + lengthX; + } + let vectorOffset = lengthOffset + 1; + + if (lengthFlag) { + vectorOffset += 1; + } + + const memberID = view.getUint16(vectorOffset); + + const headerOffset = vectorOffset + 2; + const clientProtocol = view.getUint32(headerOffset); + const association = view.getUint16(headerOffset + 4); + + const dataOffset = headerOffset + 6; + // NOTE(jwetzell): flags/lengthH + lengthL + lengthX + vector + header + const dataLength = length - (1 + 1 + (lengthFlag ? 1 : 0) + 2 + 6); + let data: SessionDataTransportPDU | Uint8Array = bytes.subarray(dataOffset, dataOffset + dataLength); + + if (clientProtocol === Protocol.SDT) { + data = decode(data); + } else { + console.error(`SDT client block contains unknown protocol: ${clientProtocol}`); + } + return { + memberID, + clientProtocol, + association, + data, + }; +} + +function decodeData( + vector: SDTVector, + bytes: Uint8Array +): + | SDTJoinData + | SDTJoinAcceptData + | SDTJoinRefuseData + | SDTWrapperData + | SDTAckData + | SDTLeavingData + | SDTGetSessionsData + | SDTNakData + | SDTConnectData + | SDTConnectAcceptData + | SDTConnectRefuseData + | SDTDisconnectData + | SDTDisconnectingData + | Uint8Array { + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + switch (vector) { + case SDTVector.JOIN: { + const destinationAddress: TransportLayerAddress = { + type: view.getUint8(30), + }; + + // TODO(jwetzell): handle IPv6, type === 2 + if (destinationAddress.type === 1) { + // IPv4 + destinationAddress.port = view.getUint16(31); + destinationAddress.address = bytes.subarray(33, 37).join('.'); + } + + const adhocExpiry = view.getUint8(45); + const joinData = { + componentID: toHex(bytes.subarray(0, 16)), + memberID: view.getUint16(16), + channelNumber: view.getUint16(18), + reciprocalChannel: view.getUint16(20), + totalSequenceNumber: view.getUint32(22), + reliableSequenceNumber: view.getUint32(26), + destinationAddress, + channelParameters: { + expiry: view.getUint8(37), + nakOutboundFlag: view.getUint8(38), + nakHoldoff: view.getUint16(39), + nakModulus: view.getUint16(41), + nakMaxWait: view.getUint16(43), + }, + adhocExpiry, + }; + return joinData; + } + case SDTVector.JOIN_ACCEPT: { + const joinData = { + leaderComponentID: toHex(bytes.subarray(0, 16)), + channelNumber: view.getUint16(16), + memberID: view.getUint16(18), + reliableSequenceNumber: view.getUint32(20), + reciprocalChannel: view.getUint16(24), + }; + return joinData; + } + case SDTVector.JOIN_REFUSE: { + const joinData = { + leaderComponentID: toHex(bytes.subarray(0, 16)), + channelNumber: view.getUint16(16), + memberID: view.getUint16(18), + reliableSequenceNumber: view.getUint32(20), + refuseCode: view.getUint8(24), + }; + + return joinData; + } + case SDTVector.REL_WRAP: + case SDTVector.UNREL_WRAP: { + const wrapperData: SDTWrapperData = { + channelNumber: view.getUint16(0), + totalSequenceNumber: view.getUint32(2), + reliableSequenceNumber: view.getUint32(6), + oldestAvailableWrapper: view.getUint32(10), + firstMemberToAck: view.getUint16(14), + lastMemberToAck: view.getUint16(16), + makThreshold: view.getUint16(18), + sdtClientBlock: decodeClientBlock(bytes.subarray(20)), + }; + + return wrapperData; + } + case SDTVector.ACK: { + return { + reliableSequenceNumber: view.getUint32(0), + }; + } + case SDTVector.LEAVING: { + return { + leaderComponentID: toHex(bytes.subarray(0, 16)), + channelNumber: view.getUint16(16), + memberID: view.getUint16(18), + reliableSequenceNumber: view.getUint32(20), + reasonCode: view.getUint8(24), + }; + } + case SDTVector.GET_SESSIONS: { + return { + componentID: toHex(bytes.subarray(0, 16)), + }; + } + case SDTVector.NAK: { + return { + leaderComponentID: toHex(bytes.subarray(0, 16)), + channelNumber: view.getUint16(16), + memberID: view.getUint16(18), + reliableSequenceNumber: view.getUint32(20), + firstMissedSequence: view.getUint32(24), + lastMissedSequence: view.getUint32(28), + }; + } + case SDTVector.DISCONNECT: + case SDTVector.CONNECT: + case SDTVector.CONNECT_ACCEPT: { + return { + protocolID: view.getUint32(0), + }; + } + case SDTVector.CONNECT_REFUSE: { + return { + protocolID: view.getUint32(0), + refuseCode: view.getUint8(4), + }; + } + case SDTVector.DISCONNECTING: { + return { + protocolID: view.getUint32(0), + reasonCode: view.getUint8(4), + }; + } + default: + console.error(`unhandled SDT vector: ${vector}`); + return bytes; + } +} + +export default { + decode, +}; diff --git a/packages/acn/src/types.ts b/packages/acn/src/types.ts new file mode 100644 index 0000000..5398bac --- /dev/null +++ b/packages/acn/src/types.ts @@ -0,0 +1,141 @@ +import { TransportLayerAddressType, SDTVector, SDTReasonCode } from './enums'; + +export type UDPPreamble = { + preambleSize: number; + postambleSize: number; + packetIdentifier: string; +}; + +export type ACNPacket = { + preamble: PreambleType; + pduBlock: PDUBlockType; + postamble: PostambleType; +}; + +export type RootLayerPDU = { + vector: number; + header: string; + data: SessionDataTransportPDU[] | Uint8Array; +}; + +export type SDTChannelParams = { + expiry: number; + nakOutboundFlag: number; + nakHoldoff: number; + nakModulus: number; + nakMaxWait: number; +}; + +export type TransportLayerAddress = { + type: TransportLayerAddressType; + port?: number; + address?: string; +}; + +export type SDTJoinData = { + componentID: string; + memberID: number; + channelNumber: number; + reciprocalChannel: number; + totalSequenceNumber: number; + reliableSequenceNumber: number; + destinationAddress: TransportLayerAddress; + channelParameters: SDTChannelParams; + adhocExpiry: number; +}; + +export type SDTJoinAcceptData = { + leaderComponentID: string; + channelNumber: number; + memberID: number; + reliableSequenceNumber: number; + reciprocalChannel: number; +}; + +export type SDTJoinRefuseData = { + leaderComponentID: string; + channelNumber: number; + memberID: number; + reliableSequenceNumber: number; + refuseCode: SDTReasonCode; +}; + +export type SDTLeavingData = { + leaderComponentID: string; + channelNumber: number; + memberID: number; + reliableSequenceNumber: number; + reasonCode: SDTReasonCode; +}; + +export type SDTWrapperData = { + channelNumber: number; + totalSequenceNumber: number; + reliableSequenceNumber: number; + oldestAvailableWrapper: number; + firstMemberToAck: number; + lastMemberToAck: number; + makThreshold: number; + sdtClientBlock?: SDTClientBlock; +}; + +export type SDTAckData = { + reliableSequenceNumber: number; +}; + +export type SDTNakData = { + leaderComponenetID: string; + channelNumber: number; + memberID: number; + reliableSequenceNumber: number; + firstMissedSequence: number; + lastMissedSequence: number; +}; + +export type SDTConnectData = { + protocolID: number; +}; + +export type SDTConnectAcceptData = SDTConnectData; +export type SDTConnectRefuseData = { + protocolID: number; + refuseCode: SDTReasonCode; +}; + +export type SDTDisconnectData = SDTConnectData; +export type SDTDisconnectingData = { + protocolID: number; + reasonCode: SDTReasonCode; +}; + +export type SDTGetSessionsData = { + componentID: string; +}; + +export type SessionDataTransportPDU = { + vector: SDTVector; + length: number; + // TODO(jwetzell): cleanup these types + data: + | SDTJoinData + | SDTJoinAcceptData + | SDTJoinRefuseData + | SDTWrapperData + | SDTAckData + | SDTLeavingData + | SDTGetSessionsData + | SDTNakData + | SDTConnectData + | SDTConnectAcceptData + | SDTConnectRefuseData + | SDTDisconnectData + | SDTDisconnectingData + | Uint8Array; +}; + +export type SDTClientBlock = { + memberID: number; + clientProtocol: number; + association: number; + data: SessionDataTransportPDU | Uint8Array; +}; diff --git a/packages/acn/src/udp.ts b/packages/acn/src/udp.ts index 8e1a8b5..0877fae 100644 --- a/packages/acn/src/udp.ts +++ b/packages/acn/src/udp.ts @@ -1,4 +1,4 @@ -import { ACNPacket, RootLayerPDU, UDPPreamble } from './models'; +import { ACNPacket, RootLayerPDU, UDPPreamble } from './types'; import pdu from './pdu'; // ANSI E1.17 - 2015 (R2020) EPI 17 diff --git a/packages/acn/src/utils.ts b/packages/acn/src/utils.ts index cd0b88d..5747042 100644 --- a/packages/acn/src/utils.ts +++ b/packages/acn/src/utils.ts @@ -1,7 +1,7 @@ -export function toHex(bytes: Uint8Array): string{ - let hex = ''; - bytes.forEach((byte)=>{ - hex += byte.toString(16).padStart(2,'0') - }) - return hex; -} \ No newline at end of file +export function toHex(bytes: Uint8Array): string { + let hex = ''; + bytes.forEach((byte) => { + hex += byte.toString(16).padStart(2, '0'); + }); + return hex; +} diff --git a/packages/acn/tests/udp-decode.test.js b/packages/acn/tests/udp-decode.test.js index fe1602b..026cec7 100644 --- a/packages/acn/tests/udp-decode.test.js +++ b/packages/acn/tests/udp-decode.test.js @@ -20,10 +20,33 @@ const goodTests = [ pduBlock: { vector: 1, header: 'b0abd82a20d24240a7cd3e49d999a4e5', - data: new Uint8Array([ - 112, 50, 4, 73, 136, 131, 52, 255, 255, 74, 69, 176, 66, 102, 183, 33, 235, 216, 155, 0, 1, 244, 136, 0, 0, 0, - 0, 244, 136, 0, 0, 244, 136, 1, 67, 216, 239, 192, 220, 207, 5, 0, 0, 2, 0, 10, 0, 20, 1, 44, - ]), + data: [ + { + vector: 4, + length: 50, + data: { + componentID: '49888334ffff4a45b04266b721ebd89b', + memberID: 1, + channelNumber: 62600, + reciprocalChannel: 0, + totalSequenceNumber: 62600, + reliableSequenceNumber: 62600, + destinationAddress: { + type: 1, + port: 17368, + address: '239.192.220.207' + }, + channelParameters: { + expiry: 5, + nakOutboundFlag: 0, + nakHoldoff: 2, + nakModulus: 10, + nakMaxWait: 20 + }, + adhocExpiry: 1, + } + } + ], }, postamble: undefined, }, @@ -71,7 +94,9 @@ const badTests = [ describe('ACN Message Decoding Pass', () => { goodTests.forEach((messageTest) => { it(messageTest.description, () => { + console.log(JSON.stringify(messageTest.expected)) const decoded = acn.decode(messageTest.bytes); + console.log(JSON.stringify(decoded)) deepEqual(decoded, messageTest.expected); }); });