-
Notifications
You must be signed in to change notification settings - Fork 104
/
index.js
304 lines (267 loc) · 9.51 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
const { Socket, isIPv4 } = require("net");
const { EIP_PORT } = require("../config");
const encapsulation = require("./encapsulation");
const CIP = require("./cip");
const { promiseTimeout } = require("../utilities");
const { lookup } = require("dns");
/**
* Low Level Ethernet/IP
*
* @class ENIP
* @extends {Socket}
* @fires ENIP#Session Registration Failed
* @fires ENIP#Session Registered
* @fires ENIP#Session Unregistered
* @fires ENIP#SendRRData Received
* @fires ENIP#SendUnitData Received
* @fires ENIP#Unhandled Encapsulated Command Received
*/
class ENIP extends Socket {
constructor() {
super();
this.state = {
TCP: { establishing: false, established: false },
session: { id: null, establishing: false, established: false },
connection: { id: null, establishing: false, established: false, seq_num: 0 },
error: { code: null, msg: null }
};
// Initialize Event Handlers for Underlying Socket Class
this._initializeEventHandlers();
}
// region Property Accessors
/**
* Returns an Object
* - <number> error code
* - <string> human readable error
*
* @readonly
* @memberof ENIP
*/
get error() {
return this.state.error;
}
/**
* Session Establishment In Progress
*
* @readonly
* @memberof ENIP
*/
get establishing() {
return this.state.session.establishing;
}
/**
* Session Established Successfully
*
* @readonly
* @memberof ENIP
*/
get established() {
return this.state.session.established;
}
/**
* Get ENIP Session ID
*
* @readonly
* @memberof ENIP
*/
get session_id() {
return this.state.session.id;
}
// endregion
// region Public Method Definitions
/**
* Initializes Session with Desired IP Address or FQDN
* and Returns a Promise with the Established Session ID
*
* @override
* @param {string} IP_ADDR - IPv4 Address (can also accept a FQDN, provided port forwarding is configured correctly.)
* @returns {Promise}
* @memberof ENIP
*/
async connect(IP_ADDR) {
if (!IP_ADDR) {
throw new Error("Controller <class> requires IP_ADDR <string>!!!");
}
await new Promise((resolve, reject) => {
lookup(IP_ADDR, (err, addr) => {
if (err) reject(new Error("DNS Lookup failed for IP_ADDR " + IP_ADDR));
if (!isIPv4(addr)) {
reject(new Error("Invalid IP_ADDR <string> passed to Controller <class>"));
}
resolve();
});
});
const { registerSession } = encapsulation;
this.state.session.establishing = true;
this.state.TCP.establishing = true;
const connectErr = new Error(
"TIMEOUT occurred while attempting to establish TCP connection with Controller."
);
// Connect to Controller and Then Send Register Session Packet
await promiseTimeout(
new Promise(resolve => {
super.connect(
EIP_PORT,
IP_ADDR,
() => {
this.state.TCP.establishing = false;
this.state.TCP.established = true;
this.write(registerSession());
resolve();
}
);
}),
10000,
connectErr
);
const sessionErr = new Error(
"TIMEOUT occurred while attempting to establish Ethernet/IP session with Controller."
);
// Wait for Session to be Registered
const sessid = await promiseTimeout(
new Promise(resolve => {
this.on("Session Registered", sessid => {
resolve(sessid);
});
this.on("Session Registration Failed", error => {
this.state.error.code = error;
this.state.error.msg = "Failed to Register Session";
resolve(null);
});
}),
10000,
sessionErr
);
// Clean Up Local Listeners
this.removeAllListeners("Session Registered");
this.removeAllListeners("Session Registration Failed");
// Return Session ID
return sessid;
}
/**
* Writes Ethernet/IP Data to Socket as an Unconnected Message
* or a Transport Class 1 Datagram
*
* NOTE: Cant Override Socket Write due to net.Socket.write
* implementation. =[. Thus, I am spinning up a new Method to
* handle it. Dont Use Enip.write, use this function instead.
*
* @param {buffer} data - Data Buffer to be Encapsulated
* @param {boolean} [connected=false]
* @param {number} [timeout=10] - Timeoue (sec)
* @param {function} [cb=null] - Callback to be Passed to Parent.Write()
* @memberof ENIP
*/
write_cip(data, connected = false, timeout = 10, cb = null) {
const { sendRRData, sendUnitData } = encapsulation;
const { session, connection } = this.state;
if (session.established) {
const packet = connected
? sendUnitData(session.id, data, connection.id, connection.seq_num)
: sendRRData(session.id, data, timeout);
if (cb) {
this.write(packet, cb);
} else {
this.write(packet);
}
}
}
/**
* Sends Unregister Session Command and Destroys Underlying TCP Socket
*
* @override
* @param {Exception} exception - Gets passed to 'error' event handler
* @memberof ENIP
*/
destroy(exception) {
const { unregisterSession } = encapsulation;
this.write(unregisterSession(this.state.session.id), () => {
this.state.session.established = false;
super.destroy(exception);
});
}
// endregion
// region Private Method Definitions
_initializeEventHandlers() {
this.on("data", this._handleDataEvent);
this.on("close", this._handleCloseEvent);
}
//endregion
// region Event Handlers
/**
* @typedef EncapsulationData
* @type {Object}
* @property {number} commandCode - Ecapsulation Command Code
* @property {string} command - Encapsulation Command String Interpretation
* @property {number} length - Length of Encapsulated Data
* @property {number} session - Session ID
* @property {number} statusCode - Status Code
* @property {string} status - Status Code String Interpretation
* @property {number} options - Options (Typically 0x00)
* @property {Buffer} data - Encapsulated Data Buffer
*/
/*****************************************************************/
/**
* Socket.on('data) Event Handler
*
* @param {Buffer} - Data Received from Socket.on('data', ...)
* @memberof ENIP
*/
_handleDataEvent(data) {
const { header, CPF, commands } = encapsulation;
const encapsulatedData = header.parse(data);
const { statusCode, status, commandCode } = encapsulatedData;
if (statusCode !== 0) {
console.log(`Error <${statusCode}>:`.red, status.red);
this.state.error.code = statusCode;
this.state.error.msg = status;
this.emit("Session Registration Failed", this.state.error);
} else {
this.state.error.code = null;
this.state.error.msg = null;
/* eslint-disable indent */
switch (commandCode) {
case commands.RegisterSession:
this.state.session.establishing = false;
this.state.session.established = true;
this.state.session.id = encapsulatedData.session;
this.emit("Session Registered", this.state.session.id);
break;
case commands.UnregisterSession:
this.state.session.established = false;
this.emit("Session Unregistered");
break;
case commands.SendRRData: {
let buf1 = Buffer.alloc(encapsulatedData.length - 6); // length of Data - Interface Handle <UDINT> and Timeout <UINT>
encapsulatedData.data.copy(buf1, 0, 6);
const srrd = CPF.parse(buf1);
this.emit("SendRRData Received", srrd);
break;
}
case commands.SendUnitData: {
let buf2 = Buffer.alloc(encapsulatedData.length - 6); // length of Data - Interface Handle <UDINT> and Timeout <UINT>
encapsulatedData.data.copy(buf2, 0, 6);
const sud = CPF.parse(buf2);
this.emit("SendUnitData Received", sud);
break;
}
default:
this.emit("Unhandled Encapsulated Command Received", encapsulatedData);
}
/* eslint-enable indent */
}
}
/**
* Socket.on('close',...) Event Handler
*
* @param {Boolean} hadError
* @memberof ENIP
*/
_handleCloseEvent(hadError) {
this.state.session.established = false;
this.state.TCP.established = false;
if (hadError) throw new Error("Socket Transmission Failure Occurred!");
}
// endregion
}
module.exports = { ENIP, CIP, encapsulation };