1 | var fs = require("fs");
|
2 | var util = require("util");
|
3 | var crypto = require("crypto");
|
4 | var path = require("path");
|
5 |
|
6 | var Radius = {};
|
7 |
|
8 | var attributes_map = {}, vendor_name_to_id = {};
|
9 | var dictionary_locations = [path.normalize(__dirname + "/../dictionaries")];
|
10 |
|
11 | const NOT_LOADED = 1;
|
12 | const LOADING = 2;
|
13 | const LOADED = 3;
|
14 |
|
15 | var dictionaries_state = NOT_LOADED;
|
16 |
|
17 | const NO_VENDOR = -1;
|
18 |
|
19 | const ATTR_ID = 0;
|
20 | const ATTR_NAME = 1;
|
21 | const ATTR_TYPE = 2;
|
22 | const ATTR_ENUM = 3;
|
23 | const ATTR_REVERSE_ENUM = 4;
|
24 | const ATTR_MODIFIERS = 5;
|
25 |
|
26 | const AUTH_START = 4;
|
27 | const AUTH_END = 20;
|
28 | const AUTH_LENGTH = 16;
|
29 |
|
30 | Radius.InvalidSecretError = function(msg, decoded, constr) {
|
31 | Error.captureStackTrace(this, constr || this);
|
32 | this.message = msg || 'Error';
|
33 | this.decoded = decoded;
|
34 | };
|
35 | util.inherits(Radius.InvalidSecretError, Error);
|
36 | Radius.InvalidSecretError.prototype.name = 'Invalid Secret Error';
|
37 |
|
38 | Radius.add_dictionary = function(file) {
|
39 | dictionary_locations.push(path.resolve(file));
|
40 | };
|
41 |
|
42 | var load_dictionaries_cbs = [];
|
43 | Radius.load_dictionaries = function(callback) {
|
44 | var self = this;
|
45 |
|
46 | if (callback) {
|
47 | load_dictionaries_cbs.push(callback);
|
48 | }
|
49 |
|
50 | if (dictionaries_state == LOADING) {
|
51 | return;
|
52 | }
|
53 |
|
54 | dictionaries_state = LOADING;
|
55 |
|
56 | var locations_to_check = dictionary_locations.length, files_to_load = 0;
|
57 | var load_dict_callback = function(more_files_to_check) {
|
58 | files_to_load += more_files_to_check;
|
59 | files_to_load -= 1;
|
60 | if (locations_to_check == 0 && files_to_load == 0) {
|
61 | dictionaries_state = LOADED;
|
62 | var cbs = load_dictionaries_cbs;
|
63 | load_dictionaries_cbs = [];
|
64 | cbs.forEach(function(cb) { cb(); });
|
65 | }
|
66 | };
|
67 |
|
68 | dictionary_locations.forEach(function(file) {
|
69 | if (callback) {
|
70 | fs.stat(file, function(err, stats) {
|
71 | if (err) throw err;
|
72 |
|
73 | if (stats.isDirectory()) {
|
74 | fs.readdir(file, function(err, fs) {
|
75 | if (err) throw err;
|
76 |
|
77 | files_to_load += fs.length;
|
78 | locations_to_check--;
|
79 | fs.forEach(function(f) {
|
80 | self.load_dictionary(file + "/" + f, load_dict_callback);
|
81 | });
|
82 | });
|
83 | } else {
|
84 | files_to_load++;
|
85 | locations_to_check--;
|
86 | self.load_dictionary(file, load_dict_callback);
|
87 | }
|
88 | });
|
89 | } else {
|
90 | if (!fs.existsSync(file)) {
|
91 | throw new Error("Invalid dictionary location: " + file);
|
92 | }
|
93 |
|
94 | if (fs.statSync(file).isDirectory()) {
|
95 | var files = fs.readdirSync(file);
|
96 | for (var j = 0; j < files.length; j++) {
|
97 | self.load_dictionary(file + "/" + files[j]);
|
98 | }
|
99 | } else {
|
100 | self.load_dictionary(file);
|
101 | }
|
102 | dictionaries_state = LOADED;
|
103 | }
|
104 | });
|
105 | };
|
106 |
|
107 | Radius.load_dictionary = function(file, callback, seen_files) {
|
108 | file = path.normalize(file);
|
109 | var self = this;
|
110 |
|
111 | if (seen_files === undefined) {
|
112 | seen_files = {};
|
113 | }
|
114 |
|
115 | if (seen_files[file]) {
|
116 | if (callback) {
|
117 | callback(0);
|
118 | }
|
119 | return;
|
120 | }
|
121 |
|
122 | seen_files[file] = true;
|
123 |
|
124 | if (callback) {
|
125 | fs.readFile(file, "ascii", function(err, contents) {
|
126 | if (err) throw err;
|
127 | var includes = self._load_dictionary(contents);
|
128 | callback(includes.length);
|
129 | includes.forEach(function (i) {
|
130 | self.load_dictionary(path.join(path.dirname(file), i), callback, seen_files);
|
131 | });
|
132 | });
|
133 | } else {
|
134 | var includes = self._load_dictionary(fs.readFileSync(file, "ascii"));
|
135 | includes.forEach(function (i) {
|
136 | self.load_dictionary(path.join(path.dirname(file), i), callback, seen_files);
|
137 | });
|
138 | }
|
139 | };
|
140 |
|
141 | Radius._load_dictionary = function(content) {
|
142 | var lines = content.split("\n");
|
143 |
|
144 | var vendor = NO_VENDOR, includes = [];
|
145 | for (var i = 0; i < lines.length; i++) {
|
146 | var line = lines[i];
|
147 |
|
148 | line = line.replace(/#.*/, "").replace(/\s+/g, " ");
|
149 |
|
150 | var match = line.match(/^\s*VENDOR\s+(\S+)\s+(\d+)/);
|
151 | if (match) {
|
152 | vendor_name_to_id[match[1]] = match[2];
|
153 | continue;
|
154 | }
|
155 |
|
156 | if ((match = line.match(/^\s*BEGIN-VENDOR\s+(\S+)/))) {
|
157 | vendor = vendor_name_to_id[match[1]];
|
158 | continue;
|
159 | }
|
160 |
|
161 | if (line.match(/^\s*END-VENDOR/)) {
|
162 | vendor = NO_VENDOR;
|
163 | continue;
|
164 | }
|
165 |
|
166 | var init_entry = function(vendor, attr_id) {
|
167 | if (!attributes_map[vendor]) {
|
168 | attributes_map[vendor] = {};
|
169 | }
|
170 |
|
171 | if (!attributes_map[vendor][attr_id]) {
|
172 | attributes_map[vendor][attr_id] = [null, null, null, {}, {}, {}];
|
173 | }
|
174 | };
|
175 |
|
176 | match = line.match(/^\s*(?:VENDOR)?ATTR(?:IBUTE)?\s+(\d+)?\s*(\S+)\s+(\d+)\s+(\S+)\s*(.+)?/);
|
177 | if (match) {
|
178 | var attr_vendor = vendor;
|
179 | if (match[1] !== undefined) {
|
180 | attr_vendor = match[1];
|
181 | }
|
182 |
|
183 | var modifiers = {};
|
184 | if (match[5] !== undefined) {
|
185 | match[5].replace(/\s*/g, "").split(",").forEach(function(m) {
|
186 | modifiers[m] = true;
|
187 | });
|
188 | }
|
189 |
|
190 | init_entry(attr_vendor, match[3]);
|
191 |
|
192 | attributes_map[attr_vendor][match[3]][ATTR_ID] = match[3];
|
193 | attributes_map[attr_vendor][match[3]][ATTR_NAME] = match[2];
|
194 | attributes_map[attr_vendor][match[3]][ATTR_TYPE] = match[4];
|
195 | attributes_map[attr_vendor][match[3]][ATTR_MODIFIERS] = modifiers;
|
196 |
|
197 | var by_name = attributes_map[attr_vendor][match[2]];
|
198 | if (by_name !== undefined) {
|
199 | var by_index = attributes_map[attr_vendor][match[3]];
|
200 | [ATTR_ENUM, ATTR_REVERSE_ENUM].forEach(function(field) {
|
201 | for (var name in by_name[field]) {
|
202 | by_index[field][name] = by_name[field][name];
|
203 | }
|
204 | });
|
205 | }
|
206 | attributes_map[attr_vendor][match[2]] = attributes_map[attr_vendor][match[3]];
|
207 |
|
208 | continue;
|
209 | }
|
210 |
|
211 | match = line.match(/^\s*(?:VENDOR)?VALUE\s+(\d+)?\s*(\S+)\s+(\S+)\s+(\d+)/);
|
212 | if (match) {
|
213 | var attr_vendor = vendor;
|
214 | if (match[1] !== undefined) {
|
215 | attr_vendor = match[1];
|
216 | }
|
217 |
|
218 | init_entry(attr_vendor, match[2]);
|
219 |
|
220 | attributes_map[attr_vendor][match[2]][ATTR_ENUM][match[4]] = match[3];
|
221 | attributes_map[attr_vendor][match[2]][ATTR_REVERSE_ENUM][match[3]] = match[4];
|
222 |
|
223 | continue;
|
224 | }
|
225 |
|
226 | if ((match = line.match(/^\s*\$INCLUDE\s+(.*)/))) {
|
227 | includes.push(match[1]);
|
228 | }
|
229 | }
|
230 |
|
231 | return includes;
|
232 | };
|
233 |
|
234 | Radius.unload_dictionaries = function() {
|
235 | attributes_map = {};
|
236 | vendor_name_to_id = {};
|
237 | dictionaries_state = NOT_LOADED;
|
238 | };
|
239 |
|
240 | Radius.attr_name_to_id = function(attr_name, vendor_id) {
|
241 | return this._attr_to(attr_name, vendor_id, ATTR_ID);
|
242 | };
|
243 |
|
244 | Radius.attr_id_to_name = function(attr_name, vendor_id) {
|
245 | return this._attr_to(attr_name, vendor_id, ATTR_NAME);
|
246 | };
|
247 |
|
248 | Radius._attr_to = function(attr, vendor_id, target) {
|
249 | if (vendor_id === undefined) {
|
250 | vendor_id = NO_VENDOR;
|
251 | }
|
252 |
|
253 | if (!attributes_map[vendor_id]) {
|
254 | return;
|
255 | }
|
256 |
|
257 | var attr_info = attributes_map[vendor_id][attr];
|
258 | if (!attr_info) {
|
259 | return;
|
260 | }
|
261 |
|
262 | return attr_info[target];
|
263 | };
|
264 |
|
265 | var code_map = {
|
266 | 1: "Access-Request",
|
267 | 2: "Access-Accept",
|
268 | 3: "Access-Reject",
|
269 | 4: "Accounting-Request",
|
270 | 5: "Accounting-Response",
|
271 | 6: "Interim-Accounting",
|
272 | 7: "Password-Request",
|
273 | 8: "Password-Ack",
|
274 | 9: "Password-Reject",
|
275 | 10: "Accounting-Message",
|
276 | 11: "Access-Challenge",
|
277 | 12: "Status-Server",
|
278 | 13: "Status-Client",
|
279 | 21: "Resource-Free-Request",
|
280 | 22: "Resource-Free-Response",
|
281 | 23: "Resource-Query-Request",
|
282 | 24: "Resource-Query-Response",
|
283 | 25: "Alternate-Resource-Reclaim-Request",
|
284 | 26: "NAS-Reboot-Request",
|
285 | 27: "NAS-Reboot-Response",
|
286 | 29: "Next-Passcode",
|
287 | 30: "New-Pin",
|
288 | 31: "Terminate-Session",
|
289 | 32: "Password-Expired",
|
290 | 33: "Event-Request",
|
291 | 34: "Event-Response",
|
292 | 40: "Disconnect-Request",
|
293 | 41: "Disconnect-ACK",
|
294 | 42: "Disconnect-NAK",
|
295 | 43: "CoA-Request",
|
296 | 44: "CoA-ACK",
|
297 | 45: "CoA-NAK",
|
298 | 50: "IP-Address-Allocate",
|
299 | 51: "IP-Address-Release"
|
300 | };
|
301 |
|
302 | var reverse_code_map = {};
|
303 | for (var code in code_map) {
|
304 | reverse_code_map[code_map[code]] = code;
|
305 | }
|
306 |
|
307 | Radius.error = function(error_msg, callback) {
|
308 | var err = error_msg;
|
309 | if (typeof(error_msg) === 'string') {
|
310 | err = new Error(error_msg);
|
311 | }
|
312 |
|
313 | if (callback) {
|
314 | callback(err, null);
|
315 | } else {
|
316 | throw err;
|
317 | }
|
318 | };
|
319 |
|
320 | Radius.decode = function(args) {
|
321 | return this.check_dictionaries(args, this._decode);
|
322 | };
|
323 |
|
324 | Radius._decode = function(args) {
|
325 | var packet = args.packet;
|
326 | if (!packet || packet.length < 4) {
|
327 | this.error("decode: packet too short", args.callback);
|
328 | return;
|
329 | }
|
330 |
|
331 | var ret = {};
|
332 |
|
333 | ret.code = code_map[packet.readUInt8(0)];
|
334 |
|
335 | if (!ret.code) {
|
336 | this.error("decode: invalid packet code '" + packet.readUInt8(0) + "'", args.callback);
|
337 | return;
|
338 | }
|
339 |
|
340 | ret.identifier = packet.readUInt8(1);
|
341 | ret.length = packet.readUInt16BE(2);
|
342 |
|
343 | if (packet.length < ret.length) {
|
344 | this.error("decode: incomplete packet", args.callback);
|
345 | return;
|
346 | }
|
347 |
|
348 | this.authenticator = ret.authenticator = packet.slice(AUTH_START, AUTH_END);
|
349 | this.secret = args.secret;
|
350 |
|
351 | var attrs = packet.slice(AUTH_END, ret.length);
|
352 | ret.attributes = {};
|
353 | ret.raw_attributes = [];
|
354 |
|
355 | try {
|
356 | this.decode_attributes(attrs, ret.attributes, NO_VENDOR, ret.raw_attributes);
|
357 | } catch(err) {
|
358 | this.error(err, args.callback);
|
359 | return;
|
360 | }
|
361 |
|
362 |
|
363 | if (ret.code != "Access-Request" && ret.code.match(/Request/)) {
|
364 | var orig_authenticator = new Buffer(AUTH_LENGTH);
|
365 | packet.copy(orig_authenticator, 0, AUTH_START, AUTH_END);
|
366 | packet.fill(0, AUTH_START, AUTH_END);
|
367 |
|
368 | var checksum = this.calculate_packet_checksum(packet, args.secret);
|
369 | orig_authenticator.copy(packet, AUTH_START);
|
370 |
|
371 | if (checksum.toString() != this.authenticator.toString()) {
|
372 | this.error(new Radius.InvalidSecretError("decode: authenticator mismatch (possible shared secret mismatch)", ret), args.callback);
|
373 | return;
|
374 | }
|
375 | }
|
376 |
|
377 | if (args.callback) {
|
378 | args.callback(null, ret);
|
379 | } else {
|
380 | return ret;
|
381 | }
|
382 | };
|
383 |
|
384 | Radius.verify_response = function(args) {
|
385 | if (!args || !Buffer.isBuffer(args.request) || !Buffer.isBuffer(args.response)) {
|
386 | this.error("verify_response: must provide raw request and response packets");
|
387 | return;
|
388 | }
|
389 |
|
390 | if (!args.secret) {
|
391 | this.error("verify_response: must specify shared secret");
|
392 | return;
|
393 | }
|
394 |
|
395 | var got_checksum = new Buffer(AUTH_LENGTH);
|
396 | args.response.copy(got_checksum, 0, AUTH_START, AUTH_END);
|
397 | args.request.copy(args.response, AUTH_START, AUTH_START, AUTH_END);
|
398 |
|
399 | var expected_checksum = this.calculate_packet_checksum(args.response, args.secret);
|
400 | got_checksum.copy(args.response, AUTH_START);
|
401 |
|
402 | return expected_checksum.toString() == args.response.slice(AUTH_START, AUTH_END).toString();
|
403 | };
|
404 |
|
405 | Radius.decode_attributes = function(data, attr_hash, vendor, raw_attrs) {
|
406 | var type, length, value, tag;
|
407 | while (data.length > 0) {
|
408 | type = data.readUInt8(0);
|
409 | length = data.readUInt8(1);
|
410 | value = data.slice(2, length);
|
411 |
|
412 | if (raw_attrs) {
|
413 | raw_attrs.push([type, value]);
|
414 | }
|
415 |
|
416 | data = data.slice(length);
|
417 | var attr_info = attributes_map[vendor] && attributes_map[vendor][type];
|
418 | if (!attr_info) {
|
419 | continue;
|
420 | }
|
421 |
|
422 | if (attr_info[ATTR_MODIFIERS]["has_tag"]) {
|
423 | var first_byte = value.readUInt8(0);
|
424 | if (first_byte <= 0x1F) {
|
425 | tag = first_byte;
|
426 | value = value.slice(1);
|
427 | } else {
|
428 | tag = undefined;
|
429 | }
|
430 | }
|
431 |
|
432 | if (attr_info[ATTR_MODIFIERS]["encrypt=1"]) {
|
433 | value = this.decrypt_field(value);
|
434 | } else {
|
435 | switch (attr_info[ATTR_TYPE]) {
|
436 | case "string":
|
437 | case "text":
|
438 |
|
439 | value = value.toString("utf8");
|
440 | break;
|
441 | case "ipaddr":
|
442 | var octets = [];
|
443 | for (var i = 0; i < value.length; i++) {
|
444 | octets.push(value[i]);
|
445 | }
|
446 | value = octets.join(".");
|
447 | break;
|
448 | case "date":
|
449 | value = new Date(value.readUInt32BE(0) * 1000);
|
450 | break;
|
451 | case "time":
|
452 | case "integer":
|
453 | if (attr_info[ATTR_MODIFIERS]["has_tag"]) {
|
454 | var buf = new Buffer([0, 0, 0, 0]);
|
455 | value.copy(buf, 1);
|
456 | value = buf;
|
457 | }
|
458 |
|
459 | value = value.readUInt32BE(0);
|
460 | value = attr_info[ATTR_ENUM][value] || value;
|
461 | break;
|
462 | }
|
463 |
|
464 | if (attr_info[ATTR_NAME] == "Vendor-Specific") {
|
465 | if (value[0] !== 0x00) {
|
466 | throw new Error("Invalid vendor id");
|
467 | }
|
468 |
|
469 | var vendor_attrs = attr_hash["Vendor-Specific"];
|
470 | if (!vendor_attrs) {
|
471 | vendor_attrs = attr_hash["Vendor-Specific"] = {};
|
472 | }
|
473 |
|
474 | this.decode_attributes(value.slice(4), vendor_attrs, value.readUInt32BE(0));
|
475 | continue;
|
476 | }
|
477 | }
|
478 |
|
479 | if (tag !== undefined) {
|
480 | value = [tag, value];
|
481 | }
|
482 |
|
483 | if (attr_hash[attr_info[ATTR_NAME]] !== undefined) {
|
484 | if (!(attr_hash[attr_info[ATTR_NAME]] instanceof Array)) {
|
485 | attr_hash[attr_info[ATTR_NAME]] = [attr_hash[attr_info[ATTR_NAME]]];
|
486 | }
|
487 |
|
488 | attr_hash[attr_info[ATTR_NAME]].push(value);
|
489 | } else {
|
490 | attr_hash[attr_info[ATTR_NAME]] = value;
|
491 | }
|
492 | }
|
493 | };
|
494 |
|
495 | Radius.decrypt_field = function(field) {
|
496 | if (field.length < 16) {
|
497 | throw new Error("Invalid password: too short");
|
498 | }
|
499 |
|
500 | if (field.length > 128) {
|
501 | throw new Error("Invalid password: too long");
|
502 | }
|
503 |
|
504 | if (field.length % 16 != 0) {
|
505 | throw new Error("Invalid password: not padded");
|
506 | }
|
507 |
|
508 | return this._crypt_field(field, true).toString("utf8");
|
509 | };
|
510 |
|
511 | Radius.encrypt_field = function(field) {
|
512 | var buf = new Buffer(field.length + 15 - ((15 + field.length) % 16));
|
513 | buf.write(field, 0, field.length);
|
514 |
|
515 |
|
516 | for (var i = field.length; i < buf.length; i++) {
|
517 | buf[i] = 0x00;
|
518 | }
|
519 |
|
520 | return this._crypt_field(buf, false);
|
521 | };
|
522 |
|
523 | Radius._crypt_field = function(field, is_decrypt) {
|
524 | var ret = new Buffer(0);
|
525 | var second_part_to_be_hashed = this.authenticator;
|
526 |
|
527 | if (this.secret === undefined) {
|
528 | throw new Error("Must provide RADIUS shared secret");
|
529 | }
|
530 |
|
531 | for (var i = 0; i < field.length; i = i + 16) {
|
532 | var hasher = crypto.createHash("md5");
|
533 | hasher.update(this.secret);
|
534 | hasher.update(second_part_to_be_hashed);
|
535 | var hash = new Buffer(hasher.digest("binary"), "binary");
|
536 |
|
537 | var xor_result = new Buffer(16);
|
538 | for (var j = 0; j < 16; j++) {
|
539 | xor_result[j] = field[i + j] ^ hash[j];
|
540 | if (is_decrypt && xor_result[j] == 0x00) {
|
541 | xor_result = xor_result.slice(0, j);
|
542 | break;
|
543 | }
|
544 | }
|
545 | ret = Buffer.concat([ret, xor_result]);
|
546 | second_part_to_be_hashed = is_decrypt ? field.slice(i, i + 16) : xor_result;
|
547 | }
|
548 |
|
549 | return ret;
|
550 | };
|
551 |
|
552 | Radius.encode_response = function(args) {
|
553 | return this.check_dictionaries(args, this._encode_response);
|
554 | };
|
555 |
|
556 | Radius._encode_response = function(args) {
|
557 | var self = this;
|
558 | var packet = args.packet;
|
559 | if (!packet) {
|
560 | this.error("encode_response: must provide packet", args.callback);
|
561 | return;
|
562 | }
|
563 |
|
564 | if (!args.attributes) {
|
565 | args.attributes = [];
|
566 | }
|
567 |
|
568 | var proxy_state_id = attributes_map[NO_VENDOR]["Proxy-State"][ATTR_ID];
|
569 | for (var i = 0; i < packet.raw_attributes.length; i++) {
|
570 | var attr = packet.raw_attributes[i];
|
571 | if (attr[0] == proxy_state_id) {
|
572 | args.attributes.push(attr);
|
573 | }
|
574 | }
|
575 |
|
576 | var response = this.encode({
|
577 | code: args.code,
|
578 | identifier: packet.identifier,
|
579 | authenticator: packet.authenticator,
|
580 | attributes: args.attributes,
|
581 | secret: args.secret,
|
582 | callback: args.callback
|
583 | });
|
584 |
|
585 | if (!args.callback) {
|
586 | return response;
|
587 | }
|
588 | };
|
589 |
|
590 | Radius.check_dictionaries = function(args, callback) {
|
591 | var self = this;
|
592 | if (dictionaries_state != LOADED) {
|
593 | if (args.callback) {
|
594 | this.load_dictionaries(function() { callback.call(self, args); });
|
595 | return;
|
596 | } else {
|
597 | this.load_dictionaries();
|
598 | }
|
599 | }
|
600 |
|
601 | return callback.call(this, args);
|
602 | };
|
603 |
|
604 | Radius.encode = function(args) {
|
605 | return this.check_dictionaries(args, this._encode);
|
606 | };
|
607 |
|
608 | Radius._encode = function(args) {
|
609 | var self = this;
|
610 | if (!args || args.code === undefined) {
|
611 | self.error("encode: must specify code", args.callback);
|
612 | return;
|
613 | }
|
614 |
|
615 | if (args.secret === undefined) {
|
616 | self.error("encode: must provide RADIUS shared secret", args.callback);
|
617 | return;
|
618 | }
|
619 |
|
620 | var packet = new Buffer(4096);
|
621 | var offset = 0;
|
622 |
|
623 | var code = reverse_code_map[args.code];
|
624 | if (code === undefined) {
|
625 | self.error("encode: invalid packet code '" + args.code + "'", args.callback);
|
626 | return;
|
627 | }
|
628 |
|
629 | packet.writeUInt8(+code, offset++);
|
630 |
|
631 | var identifier = args.identifier;
|
632 | if (identifier === undefined) {
|
633 | identifier = Math.floor(Math.random() * 256);
|
634 | }
|
635 | if (identifier > 255) {
|
636 | self.error("encode: identifier too large", args.callback);
|
637 | return;
|
638 | }
|
639 | packet.writeUInt8(identifier, offset++);
|
640 |
|
641 |
|
642 | offset += 2;
|
643 |
|
644 | var authenticator = args.authenticator;
|
645 |
|
646 | if (!authenticator) {
|
647 | if (args.code == "Access-Request") {
|
648 | if (args.callback) {
|
649 | crypto.randomBytes(AUTH_LENGTH, function(err, buf) {
|
650 | if (err) {
|
651 | self.error(err, args.callback);
|
652 | return;
|
653 | }
|
654 | self._encode_with_authenticator(args, packet, offset, buf);
|
655 | });
|
656 | return;
|
657 | } else {
|
658 | authenticator = crypto.randomBytes(AUTH_LENGTH);
|
659 | }
|
660 | } else {
|
661 | authenticator = new Buffer(AUTH_LENGTH);
|
662 | authenticator.fill(0x00);
|
663 | }
|
664 | }
|
665 | return self._encode_with_authenticator(args, packet, offset, authenticator);
|
666 | };
|
667 |
|
668 | Radius._encode_with_authenticator = function(args, packet, offset, authenticator) {
|
669 | authenticator.copy(packet, offset);
|
670 | offset += AUTH_LENGTH;
|
671 |
|
672 | this.secret = args.secret;
|
673 | this.authenticator = authenticator;
|
674 |
|
675 | try {
|
676 | offset += this.encode_attributes(packet.slice(offset), args.attributes, NO_VENDOR);
|
677 | } catch (err) {
|
678 | this.error(err, args.callback);
|
679 | return;
|
680 | }
|
681 |
|
682 |
|
683 | packet.writeUInt16BE(offset, 2);
|
684 |
|
685 | packet = packet.slice(0, offset);
|
686 |
|
687 | if (args.code != "Access-Request") {
|
688 | this.calculate_packet_checksum(packet, args.secret).copy(packet, AUTH_START);
|
689 | }
|
690 |
|
691 | if (args.callback) {
|
692 | args.callback(null, packet);
|
693 | } else {
|
694 | return packet;
|
695 | }
|
696 | };
|
697 |
|
698 | Radius.calculate_packet_checksum = function(packet, secret) {
|
699 | var hasher = crypto.createHash("md5");
|
700 | hasher.update(packet);
|
701 | hasher.update(secret);
|
702 | return new Buffer(hasher.digest("binary"), "binary");
|
703 | };
|
704 |
|
705 | Radius.encode_attributes = function(packet, attributes, vendor) {
|
706 | if (!attributes) {
|
707 | return 0;
|
708 | }
|
709 |
|
710 | if (typeof(attributes) == 'object' && !Array.isArray(attributes)) {
|
711 | var array_attributes = [];
|
712 | for (var name in attributes) {
|
713 | var val = attributes[name];
|
714 | if (typeof(val) == 'object') {
|
715 | throw new Error("Cannot have nested attributes when using hash syntax. Use array syntax instead");
|
716 | }
|
717 | array_attributes.push([name, val]);
|
718 | }
|
719 | attributes = array_attributes;
|
720 | }
|
721 |
|
722 | var offset = 0;
|
723 | for (var i = 0; i < attributes.length; i++) {
|
724 | var attr = attributes[i];
|
725 | var attr_info = attributes_map[vendor] && attributes_map[vendor][attr[0]];
|
726 | if (!attr_info && !(attr[1] instanceof Buffer)) {
|
727 | throw new Error("encode: invalid attributes - must give Buffer for " +
|
728 | "unknown attribute '" + attr[0] + "'");
|
729 | }
|
730 |
|
731 | var out_value, in_value = attr[1];
|
732 | if (in_value instanceof Buffer) {
|
733 | out_value = in_value;
|
734 | } else {
|
735 | var has_tag = attr_info[ATTR_MODIFIERS]["has_tag"] && attr.length == 3;
|
736 |
|
737 | if (has_tag) {
|
738 | in_value = attr[2];
|
739 | }
|
740 |
|
741 | if (attr_info[ATTR_MODIFIERS]["encrypt=1"]) {
|
742 | out_value = this.encrypt_field(in_value);
|
743 | } else {
|
744 | switch (attr_info[ATTR_TYPE]) {
|
745 | case "string":
|
746 | case "text":
|
747 | if (in_value.length == 0) {
|
748 | continue;
|
749 | }
|
750 | out_value = new Buffer(in_value + "", "utf8");
|
751 | break;
|
752 | case "ipaddr":
|
753 | out_value = new Buffer(in_value.split("."));
|
754 | if (out_value.length != 4) {
|
755 | throw new Error("encode: invalid IP: " + in_value);
|
756 | }
|
757 | break;
|
758 | case "date":
|
759 | in_value = in_value.getTime() / 1000;
|
760 | case "time":
|
761 | case "integer":
|
762 | out_value = new Buffer(4);
|
763 |
|
764 | in_value = attr_info[ATTR_REVERSE_ENUM][in_value] || in_value;
|
765 | if (isNaN(in_value)) {
|
766 | throw new Error("envode: invalid attribute value: " + in_value);
|
767 | }
|
768 |
|
769 | out_value.writeUInt32BE(+in_value, 0);
|
770 |
|
771 | if (has_tag) {
|
772 | out_value = out_value.slice(1);
|
773 | }
|
774 |
|
775 | break;
|
776 | default:
|
777 | if (attr_info[ATTR_NAME] != "Vendor-Specific") {
|
778 | throw new Error("encode: must provide Buffer for attribute '" + attr_info[ATTR_NAME] + "'");
|
779 | }
|
780 | }
|
781 |
|
782 |
|
783 | if (attr_info[ATTR_NAME] == "Vendor-Specific") {
|
784 | var vendor_id = isNaN(attr[1]) ? vendor_name_to_id[attr[1]] : attr[1];
|
785 | if (vendor_id === undefined) {
|
786 | throw new Error("encode: unknown vendor '" + attr[1] + "'");
|
787 | }
|
788 |
|
789 |
|
790 | packet.writeUInt8(+attr_info[ATTR_ID], offset++);
|
791 |
|
792 | var length = this.encode_attributes(packet.slice(offset + 5), attr[2], vendor_id);
|
793 |
|
794 |
|
795 | packet.writeUInt8(2 + 4 + length, offset++);
|
796 |
|
797 | packet.writeUInt32BE(+vendor_id, offset);
|
798 | offset += 4;
|
799 |
|
800 | offset += length;
|
801 | continue;
|
802 | }
|
803 | }
|
804 | }
|
805 |
|
806 |
|
807 | packet.writeUInt8(attr_info ? +attr_info[ATTR_ID] : +attr[0], offset++);
|
808 |
|
809 |
|
810 | packet.writeUInt8(2 + out_value.length + (has_tag ? 1 : 0), offset++);
|
811 |
|
812 | if (has_tag) {
|
813 | packet.writeUInt8(attr[1], offset++);
|
814 | }
|
815 |
|
816 |
|
817 | out_value.copy(packet, offset);
|
818 | offset += out_value.length;
|
819 | }
|
820 |
|
821 | return offset;
|
822 | };
|
823 |
|
824 | module.exports = Radius;
|