UNPKG

21.8 kBJavaScriptView Raw
1var fs = require("fs");
2var util = require("util");
3var crypto = require("crypto");
4var path = require("path");
5
6var Radius = {};
7
8var attributes_map = {}, vendor_name_to_id = {};
9var dictionary_locations = [path.normalize(__dirname + "/../dictionaries")];
10
11const NOT_LOADED = 1;
12const LOADING = 2;
13const LOADED = 3;
14
15var dictionaries_state = NOT_LOADED;
16
17const NO_VENDOR = -1;
18
19const ATTR_ID = 0;
20const ATTR_NAME = 1;
21const ATTR_TYPE = 2;
22const ATTR_ENUM = 3;
23const ATTR_REVERSE_ENUM = 4;
24const ATTR_MODIFIERS = 5;
25
26const AUTH_START = 4;
27const AUTH_END = 20;
28const AUTH_LENGTH = 16;
29
30Radius.InvalidSecretError = function(msg, decoded, constr) {
31 Error.captureStackTrace(this, constr || this);
32 this.message = msg || 'Error';
33 this.decoded = decoded;
34};
35util.inherits(Radius.InvalidSecretError, Error);
36Radius.InvalidSecretError.prototype.name = 'Invalid Secret Error';
37
38Radius.add_dictionary = function(file) {
39 dictionary_locations.push(path.resolve(file));
40};
41
42var load_dictionaries_cbs = [];
43Radius.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
107Radius.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
141Radius._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
234Radius.unload_dictionaries = function() {
235 attributes_map = {};
236 vendor_name_to_id = {};
237 dictionaries_state = NOT_LOADED;
238};
239
240Radius.attr_name_to_id = function(attr_name, vendor_id) {
241 return this._attr_to(attr_name, vendor_id, ATTR_ID);
242};
243
244Radius.attr_id_to_name = function(attr_name, vendor_id) {
245 return this._attr_to(attr_name, vendor_id, ATTR_NAME);
246};
247
248Radius._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
265var 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
302var reverse_code_map = {};
303for (var code in code_map) {
304 reverse_code_map[code_map[code]] = code;
305}
306
307Radius.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
320Radius.decode = function(args) {
321 return this.check_dictionaries(args, this._decode);
322};
323
324Radius._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 // "Access-Request" has a random, unpredictable authenticator
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
384Radius.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
405Radius.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 // assumes utf8 encoding for strings
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
495Radius.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
511Radius.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 // null-out the padding
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
523Radius._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
552Radius.encode_response = function(args) {
553 return this.check_dictionaries(args, this._encode_response);
554};
555
556Radius._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
590Radius.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
604Radius.encode = function(args) {
605 return this.check_dictionaries(args, this._encode);
606};
607
608Radius._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 // save room for length
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
668Radius._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 // now write the length in
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
698Radius.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
705Radius.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 // handle VSAs specially
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 // write the attribute id
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 // write in the length
795 packet.writeUInt8(2 + 4 + length, offset++);
796 // write in the vendor id
797 packet.writeUInt32BE(+vendor_id, offset);
798 offset += 4;
799
800 offset += length;
801 continue;
802 }
803 }
804 }
805
806 // write the attribute id
807 packet.writeUInt8(attr_info ? +attr_info[ATTR_ID] : +attr[0], offset++);
808
809 // write in the attribute length
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 // copy in the attribute value
817 out_value.copy(packet, offset);
818 offset += out_value.length;
819 }
820
821 return offset;
822};
823
824module.exports = Radius;