887 lines
24 KiB
JavaScript
887 lines
24 KiB
JavaScript
var fs = require("fs");
|
|
var util = require("util");
|
|
var crypto = require("crypto");
|
|
var path = require("path");
|
|
|
|
var Radius = {};
|
|
|
|
var attributes_map = {}, vendor_name_to_id = {};
|
|
var dictionary_locations = [path.normalize(__dirname + "/../dictionaries")];
|
|
|
|
const NOT_LOADED = 1;
|
|
const LOADING = 2;
|
|
const LOADED = 3;
|
|
|
|
var dictionaries_state = NOT_LOADED;
|
|
|
|
const NO_VENDOR = -1;
|
|
|
|
const ATTR_ID = 0;
|
|
const ATTR_NAME = 1;
|
|
const ATTR_TYPE = 2;
|
|
const ATTR_ENUM = 3;
|
|
const ATTR_REVERSE_ENUM = 4;
|
|
const ATTR_MODIFIERS = 5;
|
|
|
|
const AUTH_START = 4;
|
|
const AUTH_END = 20;
|
|
const AUTH_LENGTH = 16;
|
|
const MESSAGE_AUTHENTICATOR_LENGTH = 16;
|
|
|
|
Radius.InvalidSecretError = function(msg, decoded, constr) {
|
|
Error.captureStackTrace(this, constr || this);
|
|
this.message = msg || 'Error';
|
|
this.decoded = decoded;
|
|
};
|
|
util.inherits(Radius.InvalidSecretError, Error);
|
|
Radius.InvalidSecretError.prototype.name = 'Invalid Secret Error';
|
|
|
|
Radius.add_dictionary = function(file) {
|
|
dictionary_locations.push(path.resolve(file));
|
|
};
|
|
|
|
var load_dictionaries_cbs = [];
|
|
Radius.load_dictionaries = function() {
|
|
var self = this;
|
|
|
|
if (dictionaries_state == LOADED) {
|
|
return;
|
|
}
|
|
|
|
dictionary_locations.forEach(function(file) {
|
|
if (!fs.existsSync(file)) {
|
|
throw new Error("Invalid dictionary location: " + file);
|
|
}
|
|
|
|
if (fs.statSync(file).isDirectory()) {
|
|
var files = fs.readdirSync(file);
|
|
for (var j = 0; j < files.length; j++) {
|
|
self.load_dictionary(file + "/" + files[j]);
|
|
}
|
|
} else {
|
|
self.load_dictionary(file);
|
|
}
|
|
});
|
|
|
|
dictionaries_state = LOADED;
|
|
};
|
|
|
|
Radius.load_dictionary = function(file, seen_files) {
|
|
file = path.normalize(file);
|
|
var self = this;
|
|
|
|
if (seen_files === undefined) {
|
|
seen_files = {};
|
|
}
|
|
|
|
if (seen_files[file]) {
|
|
return;
|
|
}
|
|
|
|
seen_files[file] = true;
|
|
|
|
var includes = self._load_dictionary(fs.readFileSync(file, "ascii"));
|
|
includes.forEach(function (i) {
|
|
self.load_dictionary(path.join(path.dirname(file), i), seen_files);
|
|
});
|
|
};
|
|
|
|
Radius._load_dictionary = function(content) {
|
|
var lines = content.split("\n");
|
|
|
|
var vendor = NO_VENDOR, includes = [], attr_vendor;
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var line = lines[i];
|
|
|
|
line = line.replace(/#.*/, "").replace(/\s+/g, " ");
|
|
|
|
var match = line.match(/^\s*VENDOR\s+(\S+)\s+(\d+)/);
|
|
if (match) {
|
|
vendor_name_to_id[match[1]] = match[2];
|
|
continue;
|
|
}
|
|
|
|
if ((match = line.match(/^\s*BEGIN-VENDOR\s+(\S+)/))) {
|
|
vendor = vendor_name_to_id[match[1]];
|
|
continue;
|
|
}
|
|
|
|
if (line.match(/^\s*END-VENDOR/)) {
|
|
vendor = NO_VENDOR;
|
|
continue;
|
|
}
|
|
|
|
var init_entry = function(vendor, attr_id) {
|
|
if (!attributes_map[vendor]) {
|
|
attributes_map[vendor] = {};
|
|
}
|
|
|
|
if (!attributes_map[vendor][attr_id]) {
|
|
attributes_map[vendor][attr_id] = [null, null, null, {}, {}, {}];
|
|
}
|
|
};
|
|
|
|
match = line.match(/^\s*(?:VENDORATTR\s+(\d+)|ATTRIBUTE)\s+(\S+)\s+(\d+)\s+(\S+)\s*(.+)?/);
|
|
if (match) {
|
|
attr_vendor = vendor;
|
|
if (match[1] !== undefined) {
|
|
attr_vendor = match[1];
|
|
}
|
|
|
|
var modifiers = {};
|
|
if (match[5] !== undefined) {
|
|
match[5].replace(/\s*/g, "").split(",").forEach(function(m) {
|
|
modifiers[m] = true;
|
|
});
|
|
}
|
|
|
|
init_entry(attr_vendor, match[3]);
|
|
|
|
attributes_map[attr_vendor][match[3]][ATTR_ID] = match[3];
|
|
attributes_map[attr_vendor][match[3]][ATTR_NAME] = match[2];
|
|
attributes_map[attr_vendor][match[3]][ATTR_TYPE] = match[4].toLowerCase();
|
|
attributes_map[attr_vendor][match[3]][ATTR_MODIFIERS] = modifiers;
|
|
|
|
var by_name = attributes_map[attr_vendor][match[2]];
|
|
if (by_name !== undefined) {
|
|
var by_index = attributes_map[attr_vendor][match[3]];
|
|
[ATTR_ENUM, ATTR_REVERSE_ENUM].forEach(function(field) {
|
|
for (var name in by_name[field]) {
|
|
by_index[field][name] = by_name[field][name];
|
|
}
|
|
});
|
|
}
|
|
attributes_map[attr_vendor][match[2]] = attributes_map[attr_vendor][match[3]];
|
|
|
|
continue;
|
|
}
|
|
|
|
match = line.match(/^\s*(?:VENDOR)?VALUE\s+(\d+)?\s*(\S+)\s+(\S+)\s+(\d+)/);
|
|
if (match) {
|
|
attr_vendor = vendor;
|
|
if (match[1] !== undefined) {
|
|
attr_vendor = match[1];
|
|
}
|
|
|
|
init_entry(attr_vendor, match[2]);
|
|
|
|
attributes_map[attr_vendor][match[2]][ATTR_ENUM][match[4]] = match[3];
|
|
attributes_map[attr_vendor][match[2]][ATTR_REVERSE_ENUM][match[3]] = match[4];
|
|
|
|
continue;
|
|
}
|
|
|
|
if ((match = line.match(/^\s*\$INCLUDE\s+(.*)/))) {
|
|
includes.push(match[1]);
|
|
}
|
|
}
|
|
|
|
return includes;
|
|
};
|
|
|
|
Radius.unload_dictionaries = function() {
|
|
attributes_map = {};
|
|
vendor_name_to_id = {};
|
|
dictionaries_state = NOT_LOADED;
|
|
};
|
|
|
|
Radius.attr_name_to_id = function(attr_name, vendor_id) {
|
|
return this._attr_to(attr_name, vendor_id, ATTR_ID);
|
|
};
|
|
|
|
Radius.attr_id_to_name = function(attr_name, vendor_id) {
|
|
return this._attr_to(attr_name, vendor_id, ATTR_NAME);
|
|
};
|
|
|
|
Radius.vendor_name_to_id = function(vendor_name) {
|
|
return vendor_name_to_id[vendor_name];
|
|
};
|
|
|
|
Radius._attr_to = function(attr, vendor_id, target) {
|
|
if (vendor_id === undefined) {
|
|
vendor_id = NO_VENDOR;
|
|
}
|
|
|
|
if (!attributes_map[vendor_id]) {
|
|
return;
|
|
}
|
|
|
|
var attr_info = attributes_map[vendor_id][attr];
|
|
if (!attr_info) {
|
|
return;
|
|
}
|
|
|
|
return attr_info[target];
|
|
};
|
|
|
|
var code_map = {
|
|
1: "Access-Request",
|
|
2: "Access-Accept",
|
|
3: "Access-Reject",
|
|
4: "Accounting-Request",
|
|
5: "Accounting-Response",
|
|
6: "Interim-Accounting",
|
|
7: "Password-Request",
|
|
8: "Password-Ack",
|
|
9: "Password-Reject",
|
|
10: "Accounting-Message",
|
|
11: "Access-Challenge",
|
|
12: "Status-Server",
|
|
13: "Status-Client",
|
|
21: "Resource-Free-Request",
|
|
22: "Resource-Free-Response",
|
|
23: "Resource-Query-Request",
|
|
24: "Resource-Query-Response",
|
|
25: "Alternate-Resource-Reclaim-Request",
|
|
26: "NAS-Reboot-Request",
|
|
27: "NAS-Reboot-Response",
|
|
29: "Next-Passcode",
|
|
30: "New-Pin",
|
|
31: "Terminate-Session",
|
|
32: "Password-Expired",
|
|
33: "Event-Request",
|
|
34: "Event-Response",
|
|
40: "Disconnect-Request",
|
|
41: "Disconnect-ACK",
|
|
42: "Disconnect-NAK",
|
|
43: "CoA-Request",
|
|
44: "CoA-ACK",
|
|
45: "CoA-NAK",
|
|
50: "IP-Address-Allocate",
|
|
51: "IP-Address-Release"
|
|
};
|
|
|
|
var uses_random_authenticator = {
|
|
"Access-Request": true,
|
|
"Status-Server": true
|
|
};
|
|
|
|
var is_request_code = {
|
|
"Status-Server": true
|
|
};
|
|
|
|
var reverse_code_map = {};
|
|
for (var code in code_map) {
|
|
reverse_code_map[code_map[code]] = code;
|
|
if (code_map[code].match(/Request/)) {
|
|
is_request_code[code_map[code]] = true;
|
|
}
|
|
}
|
|
|
|
Radius.error = function(error_msg) {
|
|
var err = error_msg;
|
|
if (typeof(error_msg) === 'string') {
|
|
err = new Error(error_msg);
|
|
}
|
|
|
|
throw err;
|
|
};
|
|
|
|
// this is a convenience method, "decode({..., no_secret: true})" will also do the job
|
|
Radius.decode_without_secret = function(args) {
|
|
// copy args' fields without modifiying the orginal
|
|
var nargs = {no_secret: true};
|
|
for (var p in args) {
|
|
nargs[p] = args[p];
|
|
}
|
|
return this.decode(nargs, this._decode);
|
|
};
|
|
|
|
Radius.decode = function(args) {
|
|
this.load_dictionaries();
|
|
|
|
var packet = args.packet;
|
|
if (!packet || packet.length < 4) {
|
|
this.error("decode: packet too short");
|
|
return;
|
|
}
|
|
|
|
var ret = {};
|
|
|
|
ret.code = code_map[packet.readUInt8(0)];
|
|
|
|
if (!ret.code) {
|
|
this.error("decode: invalid packet code '" + packet.readUInt8(0) + "'");
|
|
return;
|
|
}
|
|
|
|
ret.identifier = packet.readUInt8(1);
|
|
ret.length = packet.readUInt16BE(2);
|
|
|
|
if (packet.length < ret.length) {
|
|
this.error("decode: incomplete packet");
|
|
return;
|
|
}
|
|
|
|
this.authenticator = ret.authenticator = packet.slice(AUTH_START, AUTH_END);
|
|
this.no_secret = args.no_secret;
|
|
this.secret = args.secret;
|
|
|
|
var attrs = packet.slice(AUTH_END, ret.length);
|
|
ret.attributes = {};
|
|
ret.raw_attributes = [];
|
|
|
|
try {
|
|
this.decode_attributes(attrs, ret.attributes, NO_VENDOR, ret.raw_attributes);
|
|
} catch(err) {
|
|
this.error(err);
|
|
return;
|
|
}
|
|
|
|
if (!uses_random_authenticator[ret.code] && is_request_code[ret.code] && !args.no_secret) {
|
|
var orig_authenticator = new Buffer(AUTH_LENGTH);
|
|
packet.copy(orig_authenticator, 0, AUTH_START, AUTH_END);
|
|
packet.fill(0, AUTH_START, AUTH_END);
|
|
|
|
var checksum = this.calculate_packet_checksum(packet, args.secret);
|
|
orig_authenticator.copy(packet, AUTH_START);
|
|
|
|
if (checksum.toString() != this.authenticator.toString()) {
|
|
this.error(new Radius.InvalidSecretError("decode: authenticator mismatch (possible shared secret mismatch)", ret));
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (is_request_code[ret.code] && ret.attributes["Message-Authenticator"] && !args.no_secret) {
|
|
this._verify_request_message_authenticator(args, ret);
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
Radius.zero_out_message_authenticator = function(attributes) {
|
|
var ma_id = this.attr_name_to_id("Message-Authenticator");
|
|
var new_attrs = attributes.slice(0);
|
|
for (var i = 0; i < new_attrs.length; i++) {
|
|
var attr = new_attrs[i];
|
|
if (attr[0] == ma_id) {
|
|
new_attrs[i] = [ma_id, new Buffer(MESSAGE_AUTHENTICATOR_LENGTH)];
|
|
new_attrs[i][1].fill(0x00);
|
|
break;
|
|
}
|
|
}
|
|
return new_attrs;
|
|
};
|
|
|
|
Radius._verify_request_message_authenticator = function(args, request) {
|
|
var reencoded = this.encode({
|
|
code: request.code,
|
|
attributes: this.zero_out_message_authenticator(request.raw_attributes),
|
|
identifier: request.identifier,
|
|
secret: args.secret
|
|
});
|
|
|
|
request.authenticator.copy(reencoded, AUTH_START);
|
|
|
|
var orig_ma = request.attributes["Message-Authenticator"];
|
|
var expected_ma = this.calculate_message_authenticator(reencoded, args.secret);
|
|
|
|
if (orig_ma.toString() != expected_ma.toString()) {
|
|
this.error(new Radius.InvalidSecretError("decode: Message-Authenticator mismatch (possible shared secret mismatch)", request));
|
|
}
|
|
};
|
|
|
|
Radius.verify_response = function(args) {
|
|
this.load_dictionaries();
|
|
|
|
if (!args || !Buffer.isBuffer(args.request) || !Buffer.isBuffer(args.response)) {
|
|
this.error("verify_response: must provide raw request and response packets");
|
|
return;
|
|
}
|
|
|
|
if (args.secret == null) {
|
|
this.error("verify_response: must specify shared secret");
|
|
return;
|
|
}
|
|
|
|
// first verify authenticator
|
|
var got_checksum = new Buffer(AUTH_LENGTH);
|
|
args.response.copy(got_checksum, 0, AUTH_START, AUTH_END);
|
|
args.request.copy(args.response, AUTH_START, AUTH_START, AUTH_END);
|
|
|
|
var expected_checksum = this.calculate_packet_checksum(args.response, args.secret);
|
|
got_checksum.copy(args.response, AUTH_START);
|
|
|
|
if (expected_checksum.toString() != args.response.slice(AUTH_START, AUTH_END).toString()) {
|
|
return false;
|
|
}
|
|
|
|
return this._verify_response_message_authenticator(args);
|
|
};
|
|
|
|
Radius._verify_response_message_authenticator = function(args) {
|
|
var parsed_request = this.decode({
|
|
packet: args.request,
|
|
secret: args.secret
|
|
});
|
|
|
|
if (parsed_request.attributes["Message-Authenticator"]) {
|
|
var parsed_response = this.decode({
|
|
packet: args.response,
|
|
secret: args.secret
|
|
});
|
|
|
|
var got_ma = parsed_response.attributes["Message-Authenticator"];
|
|
if (!got_ma) {
|
|
return false;
|
|
}
|
|
|
|
var expected_response = this.encode({
|
|
secret: args.secret,
|
|
code: parsed_response.code,
|
|
identifier: parsed_response.identifier,
|
|
attributes: this.zero_out_message_authenticator(parsed_response.raw_attributes)
|
|
});
|
|
parsed_request.authenticator.copy(expected_response, AUTH_START);
|
|
var expected_ma = this.calculate_message_authenticator(expected_response, args.secret);
|
|
if (expected_ma.toString() != got_ma.toString()) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
Radius.decode_attributes = function(data, attr_hash, vendor, raw_attrs) {
|
|
var type, length, value, tag;
|
|
while (data.length > 0) {
|
|
type = data.readUInt8(0);
|
|
length = data.readUInt8(1);
|
|
value = data.slice(2, length);
|
|
tag = undefined;
|
|
|
|
if (length < 2) {
|
|
throw new Error("invalid attribute length: " + length);
|
|
}
|
|
|
|
if (raw_attrs) {
|
|
raw_attrs.push([type, value]);
|
|
}
|
|
|
|
data = data.slice(length);
|
|
var attr_info = attributes_map[vendor] && attributes_map[vendor][type];
|
|
if (!attr_info) {
|
|
continue;
|
|
}
|
|
|
|
if (attr_info[ATTR_MODIFIERS]["has_tag"]) {
|
|
var first_byte = value.readUInt8(0);
|
|
if (first_byte <= 0x1F) {
|
|
tag = first_byte;
|
|
value = value.slice(1);
|
|
}
|
|
}
|
|
|
|
if (attr_info[ATTR_MODIFIERS]["encrypt=1"]) {
|
|
value = this.decrypt_field(value);
|
|
} else {
|
|
switch (attr_info[ATTR_TYPE]) {
|
|
case "string":
|
|
case "text":
|
|
// assumes utf8 encoding for strings
|
|
value = value.toString("utf8");
|
|
break;
|
|
case "ipaddr":
|
|
var octets = [];
|
|
for (var i = 0; i < value.length; i++) {
|
|
octets.push(value[i]);
|
|
}
|
|
value = octets.join(".");
|
|
break;
|
|
case "date":
|
|
value = new Date(value.readUInt32BE(0) * 1000);
|
|
break;
|
|
case "time":
|
|
case "integer":
|
|
if (attr_info[ATTR_MODIFIERS]["has_tag"]) {
|
|
var buf = new Buffer([0, 0, 0, 0]);
|
|
value.copy(buf, 1);
|
|
value = buf;
|
|
}
|
|
|
|
value = value.readUInt32BE(0);
|
|
value = attr_info[ATTR_ENUM][value] || value;
|
|
break;
|
|
}
|
|
|
|
if (attr_info[ATTR_NAME] == "Vendor-Specific") {
|
|
if (value[0] !== 0x00) {
|
|
throw new Error("Invalid vendor id");
|
|
}
|
|
|
|
var vendor_attrs = attr_hash["Vendor-Specific"];
|
|
if (!vendor_attrs) {
|
|
vendor_attrs = attr_hash["Vendor-Specific"] = {};
|
|
}
|
|
|
|
this.decode_attributes(value.slice(4), vendor_attrs, value.readUInt32BE(0));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (tag !== undefined) {
|
|
value = [tag, value];
|
|
}
|
|
|
|
if (attr_hash[attr_info[ATTR_NAME]] !== undefined) {
|
|
if (!(attr_hash[attr_info[ATTR_NAME]] instanceof Array)) {
|
|
attr_hash[attr_info[ATTR_NAME]] = [attr_hash[attr_info[ATTR_NAME]]];
|
|
}
|
|
|
|
attr_hash[attr_info[ATTR_NAME]].push(value);
|
|
} else {
|
|
attr_hash[attr_info[ATTR_NAME]] = value;
|
|
}
|
|
}
|
|
};
|
|
|
|
Radius.decrypt_field = function(field) {
|
|
if (this.no_secret) {
|
|
return null;
|
|
}
|
|
|
|
if (field.length < 16) {
|
|
throw new Error("Invalid password: too short");
|
|
}
|
|
|
|
if (field.length > 128) {
|
|
throw new Error("Invalid password: too long");
|
|
}
|
|
|
|
if (field.length % 16 != 0) {
|
|
throw new Error("Invalid password: not padded");
|
|
}
|
|
|
|
var decrypted = this._crypt_field(field, true);
|
|
if (decrypted === null) return null;
|
|
return decrypted.toString("utf8");
|
|
};
|
|
|
|
Radius.encrypt_field = function(field) {
|
|
var len = Buffer.byteLength(field, 'utf8');
|
|
var buf = new Buffer(len + 15 - ((15 + len) % 16));
|
|
buf.write(field, 0, len);
|
|
|
|
// null-out the padding
|
|
for (var i = len; i < buf.length; i++) {
|
|
buf[i] = 0x00;
|
|
}
|
|
|
|
return this._crypt_field(buf, false);
|
|
};
|
|
|
|
Radius._crypt_field = function(field, is_decrypt) {
|
|
var ret = new Buffer(0);
|
|
var second_part_to_be_hashed = this.authenticator;
|
|
|
|
if (this.secret === undefined) {
|
|
throw new Error("Must provide RADIUS shared secret");
|
|
}
|
|
|
|
for (var i = 0; i < field.length; i = i + 16) {
|
|
var hasher = crypto.createHash("md5");
|
|
hasher.update(this.secret);
|
|
hasher.update(second_part_to_be_hashed);
|
|
var hash = new Buffer(hasher.digest("binary"), "binary");
|
|
|
|
var xor_result = new Buffer(16);
|
|
for (var j = 0; j < 16; j++) {
|
|
xor_result[j] = field[i + j] ^ hash[j];
|
|
if (is_decrypt && xor_result[j] == 0x00) {
|
|
xor_result = xor_result.slice(0, j);
|
|
break;
|
|
}
|
|
}
|
|
ret = Buffer.concat([ret, xor_result]);
|
|
second_part_to_be_hashed = is_decrypt ? field.slice(i, i + 16) : xor_result;
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
Radius.encode_response = function(args) {
|
|
this.load_dictionaries();
|
|
|
|
var packet = args.packet;
|
|
if (!packet) {
|
|
this.error("encode_response: must provide packet");
|
|
return;
|
|
}
|
|
|
|
if (!args.attributes) {
|
|
args.attributes = [];
|
|
}
|
|
|
|
var proxy_state_id = this.attr_name_to_id("Proxy-State");
|
|
for (var i = 0; i < packet.raw_attributes.length; i++) {
|
|
var attr = packet.raw_attributes[i];
|
|
if (attr[0] == proxy_state_id) {
|
|
args.attributes.push(attr);
|
|
}
|
|
}
|
|
|
|
var response = this.encode({
|
|
code: args.code,
|
|
identifier: packet.identifier,
|
|
authenticator: packet.authenticator,
|
|
attributes: args.attributes,
|
|
secret: args.secret,
|
|
add_message_authenticator: packet.attributes["Message-Authenticator"] != null
|
|
});
|
|
|
|
return response;
|
|
};
|
|
|
|
Radius.encode = function(args) {
|
|
this.load_dictionaries();
|
|
|
|
if (!args || args.code === undefined) {
|
|
this.error("encode: must specify code");
|
|
return;
|
|
}
|
|
|
|
if (args.secret === undefined) {
|
|
this.error("encode: must provide RADIUS shared secret");
|
|
return;
|
|
}
|
|
|
|
var packet = new Buffer(4096);
|
|
var offset = 0;
|
|
|
|
var code = reverse_code_map[args.code];
|
|
if (code === undefined) {
|
|
this.error("encode: invalid packet code '" + args.code + "'");
|
|
return;
|
|
}
|
|
|
|
packet.writeUInt8(+code, offset++);
|
|
|
|
var identifier = args.identifier;
|
|
if (identifier === undefined) {
|
|
identifier = Math.floor(Math.random() * 256);
|
|
}
|
|
if (identifier > 255) {
|
|
this.error("encode: identifier too large");
|
|
return;
|
|
}
|
|
packet.writeUInt8(identifier, offset++);
|
|
|
|
// save room for length
|
|
offset += 2;
|
|
|
|
var authenticator = args.authenticator;
|
|
|
|
if (!authenticator) {
|
|
if (uses_random_authenticator[args.code]) {
|
|
authenticator = crypto.randomBytes(AUTH_LENGTH);
|
|
} else {
|
|
authenticator = new Buffer(AUTH_LENGTH);
|
|
authenticator.fill(0x00);
|
|
}
|
|
}
|
|
|
|
return this._encode_with_authenticator(args, packet, offset, authenticator);
|
|
};
|
|
|
|
Radius._encode_with_authenticator = function(args, packet, offset, authenticator) {
|
|
authenticator.copy(packet, offset);
|
|
offset += AUTH_LENGTH;
|
|
|
|
this.secret = args.secret;
|
|
this.no_secret = false;
|
|
this.authenticator = authenticator;
|
|
|
|
args.attributes = this.ensure_array_attributes(args.attributes);
|
|
|
|
var add_message_authenticator = args.add_message_authenticator;
|
|
if (add_message_authenticator == null) {
|
|
var eap_id = this.attr_name_to_id("EAP-Message");
|
|
var ma_id = this.attr_name_to_id("Message-Authenticator");
|
|
for (var i = 0; i < args.attributes.length; i++) {
|
|
var attr_id = args.attributes[i][0];
|
|
if (attr_id == eap_id || attr_id == "EAP-Message") {
|
|
add_message_authenticator = true;
|
|
} else if (attr_id == ma_id || attr_id == "Message-Authenticator") {
|
|
add_message_authenticator = false;
|
|
break;
|
|
}
|
|
}
|
|
if (add_message_authenticator == null && args.code == "Status-Server") {
|
|
add_message_authenticator = true;
|
|
}
|
|
}
|
|
|
|
if (add_message_authenticator) {
|
|
var empty_authenticator = new Buffer(MESSAGE_AUTHENTICATOR_LENGTH);
|
|
empty_authenticator.fill(0x00);
|
|
args.attributes.push(["Message-Authenticator", empty_authenticator]);
|
|
}
|
|
|
|
try {
|
|
offset += this.encode_attributes(packet.slice(offset), args.attributes, NO_VENDOR);
|
|
} catch (err) {
|
|
this.error(err);
|
|
return;
|
|
}
|
|
|
|
// now write the length in
|
|
packet.writeUInt16BE(offset, 2);
|
|
|
|
packet = packet.slice(0, offset);
|
|
|
|
var message_authenticator;
|
|
if (add_message_authenticator && !is_request_code[args.code]) {
|
|
message_authenticator = this.calculate_message_authenticator(packet, args.secret);
|
|
message_authenticator.copy(packet, offset - MESSAGE_AUTHENTICATOR_LENGTH);
|
|
}
|
|
|
|
if (!uses_random_authenticator[args.code]) {
|
|
this.calculate_packet_checksum(packet, args.secret).copy(packet, AUTH_START);
|
|
}
|
|
|
|
if (add_message_authenticator && is_request_code[args.code]) {
|
|
message_authenticator = this.calculate_message_authenticator(packet, args.secret);
|
|
message_authenticator.copy(packet, offset - MESSAGE_AUTHENTICATOR_LENGTH);
|
|
}
|
|
|
|
return packet;
|
|
};
|
|
|
|
Radius.calculate_message_authenticator = function(packet, secret) {
|
|
var hmac = crypto.createHmac('md5', secret);
|
|
hmac.update(packet);
|
|
return new Buffer(hmac.digest('binary'), 'binary');
|
|
};
|
|
|
|
Radius.calculate_packet_checksum = function(packet, secret) {
|
|
var hasher = crypto.createHash("md5");
|
|
hasher.update(packet);
|
|
hasher.update(secret);
|
|
return new Buffer(hasher.digest("binary"), "binary");
|
|
};
|
|
|
|
Radius.ensure_array_attributes = function(attributes) {
|
|
if (!attributes) {
|
|
return [];
|
|
}
|
|
|
|
if (typeof(attributes) == 'object' && !Array.isArray(attributes)) {
|
|
var array_attributes = [];
|
|
for (var name in attributes) {
|
|
var val = attributes[name];
|
|
if (typeof(val) == 'object') {
|
|
throw new Error("Cannot have nested attributes when using hash syntax. Use array syntax instead");
|
|
}
|
|
array_attributes.push([name, val]);
|
|
}
|
|
return array_attributes;
|
|
}
|
|
|
|
return attributes;
|
|
};
|
|
|
|
Radius.encode_attributes = function(packet, attributes, vendor) {
|
|
var offset = 0;
|
|
for (var i = 0; i < attributes.length; i++) {
|
|
var attr = attributes[i];
|
|
var attr_info = attributes_map[vendor] && attributes_map[vendor][attr[0]];
|
|
if (!attr_info && !(attr[1] instanceof Buffer)) {
|
|
throw new Error("encode: invalid attributes - must give Buffer for " +
|
|
"unknown attribute '" + attr[0] + "'");
|
|
}
|
|
|
|
var out_value, in_value = attr[1];
|
|
if (in_value instanceof Buffer) {
|
|
out_value = in_value;
|
|
} else {
|
|
var has_tag = attr_info[ATTR_MODIFIERS]["has_tag"] && attr.length == 3;
|
|
|
|
if (has_tag) {
|
|
in_value = attr[2];
|
|
}
|
|
|
|
if (attr_info[ATTR_MODIFIERS]["encrypt=1"]) {
|
|
out_value = this.encrypt_field(in_value);
|
|
} else {
|
|
switch (attr_info[ATTR_TYPE]) {
|
|
case "string":
|
|
case "text":
|
|
if (in_value.length == 0) {
|
|
continue;
|
|
}
|
|
out_value = new Buffer(in_value + "", "utf8");
|
|
break;
|
|
case "ipaddr":
|
|
out_value = new Buffer(in_value.split("."));
|
|
if (out_value.length != 4) {
|
|
throw new Error("encode: invalid IP: " + in_value);
|
|
}
|
|
break;
|
|
case "date":
|
|
in_value = Math.floor(in_value.getTime() / 1000);
|
|
case "time":
|
|
case "integer":
|
|
out_value = new Buffer(4);
|
|
|
|
in_value = attr_info[ATTR_REVERSE_ENUM][in_value] || in_value;
|
|
if (isNaN(in_value)) {
|
|
throw new Error("envode: invalid attribute value: " + in_value);
|
|
}
|
|
|
|
out_value.writeUInt32BE(+in_value, 0);
|
|
|
|
if (has_tag) {
|
|
out_value = out_value.slice(1);
|
|
}
|
|
|
|
break;
|
|
default:
|
|
if (attr_info[ATTR_NAME] != "Vendor-Specific") {
|
|
throw new Error("encode: must provide Buffer for attribute '" + attr_info[ATTR_NAME] + "'");
|
|
}
|
|
}
|
|
|
|
// handle VSAs specially
|
|
if (attr_info[ATTR_NAME] == "Vendor-Specific") {
|
|
var vendor_id = isNaN(attr[1]) ? vendor_name_to_id[attr[1]] : attr[1];
|
|
if (vendor_id === undefined) {
|
|
throw new Error("encode: unknown vendor '" + attr[1] + "'");
|
|
}
|
|
|
|
// write the attribute id
|
|
packet.writeUInt8(+attr_info[ATTR_ID], offset++);
|
|
|
|
var length = this.encode_attributes(packet.slice(offset + 5), attr[2], vendor_id);
|
|
|
|
// write in the length
|
|
packet.writeUInt8(2 + 4 + length, offset++);
|
|
// write in the vendor id
|
|
packet.writeUInt32BE(+vendor_id, offset);
|
|
offset += 4;
|
|
|
|
offset += length;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// write the attribute id
|
|
packet.writeUInt8(attr_info ? +attr_info[ATTR_ID] : +attr[0], offset++);
|
|
|
|
// write in the attribute length
|
|
packet.writeUInt8(2 + out_value.length + (has_tag ? 1 : 0), offset++);
|
|
|
|
if (has_tag) {
|
|
packet.writeUInt8(attr[1], offset++);
|
|
}
|
|
|
|
// copy in the attribute value
|
|
out_value.copy(packet, offset);
|
|
offset += out_value.length;
|
|
}
|
|
|
|
return offset;
|
|
};
|
|
|
|
module.exports = Radius;
|