forked from TooTallNate/node-nat-pmp
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
369 lines (318 loc) · 8.83 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
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
/**
* Node.js implementation of the NAT Port Mapping Protocol (a.k.a NAT-PMP).
*
* References:
* http://miniupnp.free.fr/nat-pmp.html
* http://wikipedia.org/wiki/NAT_Port_Mapping_Protocol
* http://tools.ietf.org/html/draft-cheshire-nat-pmp-03
*/
/**
* Module dependencies.
*/
var dgram = require('dgram');
var assert = require('assert');
var debug = require('debug')('nat-pmp');
var inherits = require('util').inherits;
var EventEmitter = require('events').EventEmitter;
/**
* The ports defined in draft-cheshire-nat-pmp-03 to send NAT-PMP requests to.
*/
exports.CLIENT_PORT = 5350;
exports.SERVER_PORT = 5351;
/**
* The opcodes for client requests.
*/
exports.OP_EXTERNAL_IP = 0;
exports.OP_MAP_UDP = 1;
exports.OP_MAP_TCP = 2;
exports.SERVER_DELTA = 128;
/**
* Map of result codes the gateway sends back when mapping a port.
*/
exports.RESULT_CODES = {
0: 'Success',
1: 'Unsupported Version',
2: 'Not Authorized/Refused (gateway may have NAT-PMP disabled)',
3: 'Network Failure (gateway may have not obtained a DHCP lease)',
4: 'Out of Resources (no ports left)',
5: 'Unsupported opcode'
};
/**
* Creates a Client instance. Familiar API to `net.connect()`.
*/
exports.connect = function (gateway) {
var client = new Client(gateway);
process.nextTick(function () {
client.connect();
});
return client;
};
/**
* The NAT-PMP "Client" class.
*/
function Client (gateway) {
if (!(this instanceof Client)) {
return new Client(gateway);
}
debug('creating new Client instance for gateway', gateway);
EventEmitter.call(this);
this._queue = [];
this.listening = false;
this.gateway = gateway;
this.socket = dgram.createSocket('udp4');
on('listening', this);
on('message', this);
on('close', this);
on('error', this);
}
inherits(Client, EventEmitter);
exports.Client = Client;
/**
* Binds to the nat-pmp Client port.
*/
Client.prototype.connect = function () {
debug('Client#connect()');
if (this._connecting) {
return false;
}
this._connecting = true;
this.socket.bind(exports.CLIENT_PORT);
};
/**
* Queues a UDP request to be send to the gateway device.
*/
Client.prototype.request = function (op, obj, cb) {
if (typeof obj === 'function') {
cb = obj;
obj = null;
}
debug('Client#request()', [op, obj, cb]);
var buf;
var size;
var pos = 0;
switch (op) {
case exports.OP_MAP_UDP:
case exports.OP_MAP_TCP:
if (!obj) {
throw new Error('mapping a port requires an "options" object');
}
var internal = +(obj.private || obj.internal || 0);
if (internal !== (internal | 0) || internal < 0) {
throw new Error('the "private" port must be a whole integer >= 0');
}
var external = +(obj.public || obj.external || 0);
if (external !== (external | 0) || external < 0) {
throw new Error('the "public" port must be a whole integer >= 0');
}
var ttl = +(obj.ttl);
if (ttl !== (ttl | 0)) {
debug('using default "ttl" value of 3600');
ttl = 3600;
}
size = 12;
buf = new Buffer(size);
buf.writeUInt8(0, pos); pos++; // Vers = 0
buf.writeUInt8(op, pos); pos++; // OP = x
buf.writeUInt16BE(0, pos); pos+=2; // Reserved (MUST be zero)
buf.writeUInt16BE(internal, pos); pos+=2; // Internal Port
buf.writeUInt16BE(external, pos); pos+=2; // Requested External Port
buf.writeUInt32BE(ttl, pos); pos+=4; // Requested Port Mapping Lifetime in Seconds
break;
case exports.OP_EXTERNAL_IP:
default:
if (op !== exports.OP_EXTERNAL_IP) {
debug('WARN: invalid opcode given', op);
}
size = 2;
buf = new Buffer(size);
buf.writeUInt8(0, pos); pos++; // Vers = 0
buf.writeUInt8(op, pos); pos++; // OP = x
}
assert.equal(pos, size, 'buffer not fully written!');
// queue out the request
this._queue.push({ op: op, buf: buf, cb: cb });
this._next();
};
/**
* Sends a request to the server for the current external IP address.
*/
Client.prototype.externalIp = function (cb) {
this.request(exports.OP_EXTERNAL_IP, cb);
};
/**
* Sets up a new port mapping.
*/
Client.prototype.portMapping = function (opts, cb) {
var opcode;
switch (String(opts.type || 'tcp').toLowerCase()) {
case 'tcp':
opcode = exports.OP_MAP_TCP;
break;
case 'udp':
opcode = exports.OP_MAP_UDP;
break;
default:
throw new Error('"type" must be either "tcp" or "udp"');
}
this.request(opcode, opts, cb);
};
/**
* To unmap a port you simply set the TTL to 0.
*/
Client.prototype.portUnmapping = function (opts, cb) {
opts.ttl = 0;
return this.portMapping(opts, cb);
};
/**
* Processes the next request if the socket is listening.
*/
Client.prototype._next = function () {
debug('Client#_next()');
var req = this._queue[0];
if (!req) {
debug('_next: nothing to process');
return;
}
if (!this.listening) {
debug('_next: not "listening" yet, cannot send out request yet');
if (!this._connecting) {
this.connect();
}
return;
}
if (this._reqActive) {
debug('_next: already an active request so waiting...');
return;
}
this._reqActive = true;
this._req = req;
var self = this;
var buf = req.buf;
var size = buf.length;
var port = exports.SERVER_PORT;
var gateway = this.gateway;
debug('_next: sending request', buf, gateway);
this.socket.send(buf, 0, size, port, gateway, function (err, bytes) {
if (err) {
self.onerror(err);
} else if (bytes !== size) {
self.onerror(new Error('Entire request buffer not sent. This should not happen!'));
}
});
};
/**
* Closes the underlying socket.
*/
Client.prototype.close = function () {
debug('Client#close()');
if (this.socket) {
this.socket.close();
}
};
/**
* Called for the underlying socket's "listening" event.
*/
Client.prototype.onlistening = function () {
debug('Client#onlistening()');
this.listening = true;
this._connecting = false;
this.emit('listening');
this._next();
};
/**
* Called for the underlying socket's "message" event.
*/
Client.prototype.onmessage = function (msg, rinfo) {
// Ignore message if we're not expecting it
if (this._queue.length === 0) return;
debug('Client#onmessage()', [msg, rinfo]);
function cb (err) {
debug('invoking "req" callback');
self._reqActive = false;
if (err) {
if (req.cb) {
req.cb.call(self, err);
} else {
self.emit('error', err);
}
} else if (req.cb) {
req.cb.apply(self, arguments);
}
self._next();
}
var self = this;
var req = this._queue[0];
var parsed = { msg: msg };
var pos = 0;
parsed.vers = msg.readUInt8(pos); pos++;
parsed.op = msg.readUInt8(pos); pos++;
if (parsed.op - exports.SERVER_DELTA !== req.op) {
debug('onmessage: WARN: got unexpected message opcode; ignoring', parsed.op);
return;
}
// if we got here, then we're gonna invoke the request's callback,
// so shift this request off of the queue.
debug('removing "req" off of the queue');
this._queue.shift();
if (parsed.vers !== 0) {
cb(new Error('"vers" must be 0. Got: ' + parsed.vers));
return;
}
// common fields
parsed.resultCode = msg.readUInt16BE(pos); pos += 2;
parsed.resultMessage = exports.RESULT_CODES[parsed.resultCode];
parsed.epoch = msg.readUInt32BE(pos); pos += 4;
if (parsed.resultCode === 0) {
// success response
switch (req.op) {
case exports.OP_EXTERNAL_IP:
parsed.ip = [];
parsed.ip.push(msg.readUInt8(pos)); pos++;
parsed.ip.push(msg.readUInt8(pos)); pos++;
parsed.ip.push(msg.readUInt8(pos)); pos++;
parsed.ip.push(msg.readUInt8(pos)); pos++;
break;
case exports.OP_MAP_UDP:
case exports.OP_MAP_TCP:
parsed.private = parsed.internal = msg.readUInt16BE(pos); pos += 2;
parsed.public = parsed.external = msg.readUInt16BE(pos); pos += 2;
parsed.ttl = msg.readUInt32BE(pos); pos += 4;
parsed.type = req.op === 1 ? 'udp' : 'tcp';
break;
default:
return cb(new Error('unknown OP code: ' + req.op));
}
assert.equal(msg.length, pos);
cb(null, parsed);
} else {
// error response
var err = new Error(parsed.resultMessage);
err.code = parsed.resultCode;
cb(err);
}
};
/**
* Called for the underlying socket's "close" event.
*/
Client.prototype.onclose = function () {
debug('Client#onclose()');
this.listening = false;
this.socket = null;
};
/**
* Called for the underlying socket's "error" event.
*/
Client.prototype.onerror = function (err) {
debug('Client#onerror()', [err]);
if (this._req && this._req.cb) {
this._req.cb(err);
} else {
this.emit('error', err);
}
};
function on (name, target) {
target.socket.on(name, function () {
debug('on: socket event %j', name);
return target['on' + name].apply(target, arguments);
});
}