├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── dictionaries ├── dictionary.rfc2865 ├── dictionary.rfc2866 ├── dictionary.rfc2867 ├── dictionary.rfc2868 ├── dictionary.rfc2869 ├── dictionary.rfc3162 ├── dictionary.rfc3576 ├── dictionary.rfc3580 ├── dictionary.rfc4072 ├── dictionary.rfc4372 ├── dictionary.rfc4603 ├── dictionary.rfc4675 ├── dictionary.rfc4679 ├── dictionary.rfc4818 ├── dictionary.rfc4849 ├── dictionary.rfc5090 ├── dictionary.rfc5176 ├── dictionary.rfc5580 ├── dictionary.rfc5607 └── dictionary.rfc5904 ├── examples ├── auth_client.js └── auth_server.js ├── lib └── radius.js ├── package.json ├── renovate.json └── test ├── captures ├── aruba_mac_auth.packet ├── cisco_accounting.packet ├── cisco_accounting_response.packet ├── cisco_mac_auth.packet ├── cisco_mac_auth_reject.packet ├── eap_request.packet ├── invalid_register.packet └── motorola_accounting.packet ├── dictionaries ├── dictionary.airespace ├── dictionary.aruba ├── dictionary.number_vendor_name ├── dictionary.test1 ├── dictionary.test2 └── dictionary.test_tunnel_type └── radius.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - lts/* 4 | - 6 5 | - "0.10" 6 | - "0.12" 7 | - "4.3" 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Nearbuy Systems 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of Nearbuy Systems nor the names of its contributors 12 | may be used to endorse or promote products derived from this software 13 | without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL NEARBUY SYSTEMS BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-radius [![Build Status](https://secure.travis-ci.org/retailnext/node-radius.png)](http://travis-ci.org/retailnext/node-radius) - A RADIUS library for node.js 2 | 3 | node-radius is a RADIUS packet encoding/decoding library for node.js written in Javascript. With node-radius you can easily decode received packets, encode packets to send, and prepare responses to received packets. node-radius supports both RADIUS authentication and RADIUS accounting packets. 4 | 5 | node-radius requires node.js v0.8.0. To install node-radius, simply run `npm install radius` in your project directory. 6 | 7 | Let's look at some examples of how to use node-radius: 8 | 9 | var radius = require('radius'); 10 | 11 | // ... receive raw_packet from UDP socket 12 | 13 | var decoded = radius.decode({ packet: raw_packet, secret: "shared_secret" }); 14 | 15 | "decoded" might look something like this: 16 | 17 | { 18 | code: 'Access-Request', 19 | identifer: 123, 20 | length: 250, 21 | attributes: { 22 | 'NAS-IP-Address': '10.1.2.3', 23 | 'User-Name': 'jlpicard', 24 | 'User-Password': 'beverly123', 25 | 'Vendor-Specific': { 26 | 'Federation-Starship': 'Enterprise' 27 | } 28 | } 29 | } 30 | 31 | To prepare your response packet, use the encode_response function: 32 | 33 | var response = radius.encode_response({ 34 | packet: decoded, 35 | code: "Access-Accept", 36 | secret: "section31" 37 | }); 38 | 39 | To prepare a stand-alone packet, try this: 40 | 41 | var packet = radius.encode({ 42 | code: "Access-Request", 43 | secret: "obsidian order", 44 | attributes: [ 45 | ['NAS-IP-Address', '10.5.5.5'], 46 | ['User-Name', 'egarak'], 47 | ['User-Password', 'tailoredfit'], 48 | ['Vendor-Specific', 555, [['Real-Name', 'arobinson']]] 49 | ] 50 | }); 51 | 52 | 53 | ## Method descriptions: 54 | 55 | ### radius.decode(\) 56 | 57 | decode takes as input an object with the following fields: 58 | 59 | - packet (required): a Buffer containing the raw UDP RADIUS packet (as read off a socket) 60 | - secret (required): a String containing the RADIUS shared secret 61 | 62 | Using the dictionaries available, decode parses the raw packet and yields an object representation of the packet. The object has the following fields: 63 | 64 | - code: string representation of the packet code ("Access-Request", "Accounting-Response", etc) 65 | - identifier: packet identifier number (used for duplicate packet detection) 66 | - length: RADIUS packet length 67 | - attributes: an object containing all attributes node-radius knew how to parse. If an attribute is repeated, its value in the "attributes" object will become an Array containing each value. Unfortunately the dictionary files do not specify which attributes are repeatable, so if an attribute might be repeated then you need to check if the value in "attributes" is a scalar value or an Array. 68 | - raw_attributes: an array of arrays containing each raw attribute (attribute type and a Buffer containing the attribute value). This is mainly used by node-radius for generating the response packet, and would only be useful to you if you are missing relevant dictionaries and/or want to decode attributes yourself. 69 | 70 | When decoding requests (e.g. "Access-Request", "Accounting-Request"), decode will automatically verify the request authenticator and the Message-Authenticator attribute, if present. If the request doesn't check out, decode will raise an error. The error, an instance of Radius.InvalidSecretError, has a "decoded" field you can use to inspect the decoded but invalid message. The most common reason for an incorrect authenticator is using the wrong shared secret. 71 | 72 | ### radius.decode_without_secret(\) 73 | 74 | Identical to decode, but does not need the secret. This can be useful to "pre-decode" a message, in order to look-up (or calculate) the secret to be used to properly decode the message later. 75 | 76 | A message decoded without a secret will contain null values for encrypted fields (typically passwords, etc.). 77 | 78 | ### radius.encode(\) 79 | 80 | encode takes an object for arguments and returns a Buffer ready to be sent over the wire. The accepted arguments are: 81 | 82 | - code (required): string representation of the packet code ("Access-Request", "Accounting-Response", etc) 83 | - secret (required): RADIUS shared secret 84 | - identifier (optional): packet identifer number (defaults to a random number from 0 to 255) 85 | - attributes (optional): RADIUS attributes you want to add to the packet 86 | - add_message_authenticator (optional): a boolean value controlling whether the library adds the Message-Authenticator HMAC to the packet. See below for more details. 87 | 88 | encode will automatically add the Message-Authenticator when: 89 | 90 | - encoding a message with an "EAP-Message" attribute 91 | - encoding a "Status-Server" message 92 | - you manually pass the "add_message_authenticator: true" option 93 | 94 | encode will not add the Message-Authenticator when: 95 | 96 | - the attributes already contain a "Message-Authenticator" attribute 97 | - you manually pass the "add_message_authenticator: false" option 98 | 99 | The attributes will typically be like the following (see above example): 100 | 101 | attributes: [ 102 | [, ], 103 | ... 104 | ] 105 | 106 | If you don't care about attribute ordering, you can use a hash for the attributes: 107 | 108 | attributes: { 109 | : , 110 | ... 111 | } 112 | 113 | If you want to send attributes that you haven't loaded a dictionary for, you can do: 114 | 115 | attributes: [ 116 | [, ], 117 | ... 118 | ] 119 | 120 | Where the first item is the numeric attribute id and the second item is just a Buffer containing the value of the attribute (not including length). 121 | 122 | You can specify Vendor-Specific attributes like so: 123 | 124 | attributes: [ 125 | ['Vendor-Specific', , [ 126 | [, ], 127 | [, ] 128 | ], 129 | ... 130 | ] 131 | 132 | Or if you want each vendor attribute as a separate attribute, try this: 133 | 134 | attributes: [ 135 | ['Vendor-Specific', , [[, ]]], 136 | ['Vendor-Specific', , [[, ]]] 137 | ... 138 | ] 139 | 140 | Like regular attributes, you can also specify the attribute id and a raw Buffer value for VSAs. If your dictionary specifies vendor attributes using the BEGIN-VENDOR/END-VENDOR format, you can use the symbolic vendor name as defined in the dictionary in place of the numeric \. 141 | 142 | You can specify the tag field-attribute like so (see RFC2868): 143 | 144 | attributes: [ 145 | [, , ], 146 | ... 147 | ] 148 | 149 | If the attribute has an optional tag and you don't want to send it, then only specify the \ and the \. 150 | 151 | ### radius.encode\_response(\) 152 | 153 | encode_response prepares a response packet based on previously received and decoded packet. "args" is an object with the following properties: 154 | 155 | - packet (required): the output of a previous call to radius.decode 156 | - code (required): String representation of the packet code ("Access-Reject, "Accounting-Response", etc) 157 | - attributes (optional): RADIUS attributes you want to add to the packet 158 | 159 | encode_response does a few things for you to prepare the response: 160 | 161 | 1. sets the response packet's message identifier to the identifer of the previously received packet 162 | 1. copies any "Proxy-State" attributes from the previously received packet into the response packet 163 | 1. calculates the appropriate response authenticator based on the request's authenticator 164 | 1. calculates and adds a Message-Authenticator attribute if the request contained one 165 | 166 | ### radius.verify\_response(\) 167 | 168 | verify_response checks the authenticator and Message-Authenticator attribute, if applicable, of a response packet you receive. It returns true if the packet checks out, and false otherwise (likely because the other side's shared secret is wrong). "args" is an object with the following properties: 169 | 170 | - request (required): the request packet you previously sent (should be the raw packet, i.e. the output of a call to radius.encode) 171 | - response (required): the response you received to your request packet (again, the raw packet) 172 | - secret (required): RADIUS shared secret 173 | 174 | This method is useful if you are acting as the NAS. For example, if you send an "Access-Request", you can use this method to verify the response you get ("Reject" or "Accept") is legitimate. 175 | 176 | Note that if the request contained a Message-Authenticator, the response must also contain a Message-Authenticator. 177 | 178 | ## Dictionaries 179 | 180 | node-radius supports reading freeradius-style RADIUS dictionary files. node-radius comes with a slew of RFC dictionary files, so you should only worry about adding any vendor-specific dictionary files you have. node-radius will load all the dictionaries it knows about (the default RFC ones and any you added) automatically the first time it needs to, so you should add your dictionaries before you start to use the module. 181 | 182 | ### radius.add_dictionary(\) 183 | 184 | To add a dictionary to be loaded, use the **add_dictionary** function: 185 | 186 | var radius = require('radius'); 187 | 188 | radius.add_dictionary('/path/to/my/dictionary'); 189 | 190 | add\_dictionary takes either a file or a directory (given a directory, it assumes everything in the directory is a dictionary file). add\_dictionary does not block or perform any IO. It simply adds the given path to a list which is used to load dictionaries later. 191 | 192 | node-radius supports reading both the VENDORATTR and the BEGIN-VENDOR/END-VENDOR style for defining VSAs. node-radius also supports reading the following attribute modifiers: has_tag, encrypt=1. 193 | 194 | node-radius will also follow "$INCLUDE" directives inside of dictionary files (to load other dictionary files). 195 | 196 | ## Example usage 197 | 198 | The following is an example of a simple radius authentication server: 199 | 200 | var radius = require('radius'); 201 | var dgram = require("dgram"); 202 | 203 | var secret = 'radius_secret'; 204 | var server = dgram.createSocket("udp4"); 205 | 206 | server.on("message", function (msg, rinfo) { 207 | var code, username, password, packet; 208 | packet = radius.decode({packet: msg, secret: secret}); 209 | 210 | if (packet.code != 'Access-Request') { 211 | console.log('unknown packet type: ', packet.code); 212 | return; 213 | } 214 | 215 | username = packet.attributes['User-Name']; 216 | password = packet.attributes['User-Password']; 217 | 218 | console.log('Access-Request for ' + username); 219 | 220 | if (username == 'jlpicard' && password == 'beverly123') { 221 | code = 'Access-Accept'; 222 | } else { 223 | code = 'Access-Reject'; 224 | } 225 | 226 | var response = radius.encode_response({ 227 | packet: packet, 228 | code: code, 229 | secret: secret 230 | }); 231 | 232 | console.log('Sending ' + code + ' for user ' + username); 233 | server.send(response, 0, response.length, rinfo.port, rinfo.address, function(err, bytes) { 234 | if (err) { 235 | console.log('Error sending response to ', rinfo); 236 | } 237 | }); 238 | }); 239 | 240 | server.on("listening", function () { 241 | var address = server.address(); 242 | console.log("radius server listening " + 243 | address.address + ":" + address.port); 244 | }); 245 | 246 | server.bind(1812); 247 | 248 | Client and server examples can be found in the examples directory. 249 | 250 | ## Important notes: 251 | 252 | - node-radius in general does _not_ perform "higher-level" protocol validation, so for example node-radius will not complain if you encode an Access-Request packet but fail to include a NAS-IP-Address or NAS-Identifier. 253 | - node-radius in general assumes most strings are UTF-8 encoded. This will work fine for ASCII and UTF-8 strings, but will not work for other encodings. At some point I might add an "encoding" option to override this default encoding, and/or a "raw" mode that just deals with Buffers (rather than Strings) when the encoding is not known. 254 | - node-radius does not support non-standard VSAs (where type or length field for attributes are not one octet each). 255 | - node-radius does not support special decoding/encoding for the following attribute types: ipv6addr, ifid, ipv6prefix, short. If node-radius encounters a type it doesn't support, node-radius will return a raw Buffer when decoding, and expect a Buffer when encoding. 256 | - node-radius does not support any password encryption types other than that defined by RFC2865 for User-Password (e.g. does not support Tunnel-Password). 257 | 258 | But, on the plus-side, unlike many other RADIUS libraries node-radius supports encrypting/decrypting passwords longer than 16 bytes! 259 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc2865: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | # 3 | # Attributes and values defined in RFC 2865. 4 | # http://www.ietf.org/rfc/rfc2865.txt 5 | # 6 | # $Id: dictionary.rfc2865 28946 2009-07-06 12:39:58Z wmeier $ 7 | # 8 | ATTRIBUTE User-Name 1 string 9 | ATTRIBUTE User-Password 2 string encrypt=1 10 | ATTRIBUTE CHAP-Password 3 octets 11 | ATTRIBUTE NAS-IP-Address 4 ipaddr 12 | ATTRIBUTE NAS-Port 5 integer 13 | ATTRIBUTE Service-Type 6 integer 14 | ATTRIBUTE Framed-Protocol 7 integer 15 | ATTRIBUTE Framed-IP-Address 8 ipaddr 16 | ATTRIBUTE Framed-IP-Netmask 9 ipaddr 17 | ATTRIBUTE Framed-Routing 10 integer 18 | ATTRIBUTE Filter-Id 11 string 19 | ATTRIBUTE Framed-MTU 12 integer 20 | ATTRIBUTE Framed-Compression 13 integer 21 | ATTRIBUTE Login-IP-Host 14 ipaddr 22 | ATTRIBUTE Login-Service 15 integer 23 | ATTRIBUTE Login-TCP-Port 16 integer 24 | # Attribute 17 is undefined 25 | ATTRIBUTE Reply-Message 18 string 26 | ATTRIBUTE Callback-Number 19 string 27 | ATTRIBUTE Callback-Id 20 string 28 | # Attribute 21 is undefined 29 | ATTRIBUTE Framed-Route 22 string 30 | ATTRIBUTE Framed-IPX-Network 23 ipaddr 31 | ATTRIBUTE State 24 octets 32 | ATTRIBUTE Class 25 octets 33 | ATTRIBUTE Vendor-Specific 26 octets 34 | ATTRIBUTE Session-Timeout 27 integer 35 | ATTRIBUTE Idle-Timeout 28 integer 36 | ATTRIBUTE Termination-Action 29 integer 37 | ATTRIBUTE Called-Station-Id 30 string 38 | ATTRIBUTE Calling-Station-Id 31 string 39 | ATTRIBUTE NAS-Identifier 32 string 40 | ATTRIBUTE Proxy-State 33 octets 41 | ATTRIBUTE Login-LAT-Service 34 string 42 | ATTRIBUTE Login-LAT-Node 35 string 43 | ATTRIBUTE Login-LAT-Group 36 octets 44 | ATTRIBUTE Framed-AppleTalk-Link 37 integer 45 | ATTRIBUTE Framed-AppleTalk-Network 38 integer 46 | ATTRIBUTE Framed-AppleTalk-Zone 39 string 47 | 48 | ATTRIBUTE CHAP-Challenge 60 octets 49 | ATTRIBUTE NAS-Port-Type 61 integer 50 | ATTRIBUTE Port-Limit 62 integer 51 | ATTRIBUTE Login-LAT-Port 63 string 52 | 53 | # 54 | # Integer Translations 55 | # 56 | 57 | # Service types 58 | 59 | VALUE Service-Type Login-User 1 60 | VALUE Service-Type Framed-User 2 61 | VALUE Service-Type Callback-Login-User 3 62 | VALUE Service-Type Callback-Framed-User 4 63 | VALUE Service-Type Outbound-User 5 64 | VALUE Service-Type Administrative-User 6 65 | VALUE Service-Type NAS-Prompt-User 7 66 | VALUE Service-Type Authenticate-Only 8 67 | VALUE Service-Type Callback-NAS-Prompt 9 68 | VALUE Service-Type Call-Check 10 69 | VALUE Service-Type Callback-Administrative 11 70 | 71 | # Framed Protocols 72 | 73 | VALUE Framed-Protocol PPP 1 74 | VALUE Framed-Protocol SLIP 2 75 | VALUE Framed-Protocol ARAP 3 76 | VALUE Framed-Protocol Gandalf-SLML 4 77 | VALUE Framed-Protocol Xylogics-IPX-SLIP 5 78 | VALUE Framed-Protocol X.75-Synchronous 6 79 | 80 | # Framed Routing Values 81 | 82 | VALUE Framed-Routing None 0 83 | VALUE Framed-Routing Broadcast 1 84 | VALUE Framed-Routing Listen 2 85 | VALUE Framed-Routing Broadcast-Listen 3 86 | 87 | # Framed Compression Types 88 | 89 | VALUE Framed-Compression None 0 90 | VALUE Framed-Compression Van-Jacobson-TCP-IP 1 91 | VALUE Framed-Compression IPX-Header-Compression 2 92 | VALUE Framed-Compression Stac-LZS 3 93 | 94 | # Login Services 95 | 96 | VALUE Login-Service Telnet 0 97 | VALUE Login-Service Rlogin 1 98 | VALUE Login-Service TCP-Clear 2 99 | VALUE Login-Service PortMaster 3 100 | VALUE Login-Service LAT 4 101 | VALUE Login-Service X25-PAD 5 102 | VALUE Login-Service X25-T3POS 6 103 | VALUE Login-Service TCP-Clear-Quiet 8 104 | 105 | # Login-TCP-Port (see /etc/services for more examples) 106 | 107 | VALUE Login-TCP-Port Telnet 23 108 | VALUE Login-TCP-Port Rlogin 513 109 | VALUE Login-TCP-Port Rsh 514 110 | 111 | # Termination Options 112 | 113 | VALUE Termination-Action Default 0 114 | VALUE Termination-Action RADIUS-Request 1 115 | 116 | # NAS Port Types 117 | 118 | VALUE NAS-Port-Type Async 0 119 | VALUE NAS-Port-Type Sync 1 120 | VALUE NAS-Port-Type ISDN 2 121 | VALUE NAS-Port-Type ISDN-V120 3 122 | VALUE NAS-Port-Type ISDN-V110 4 123 | VALUE NAS-Port-Type Virtual 5 124 | VALUE NAS-Port-Type PIAFS 6 125 | VALUE NAS-Port-Type HDLC-Clear-Channel 7 126 | VALUE NAS-Port-Type X.25 8 127 | VALUE NAS-Port-Type X.75 9 128 | VALUE NAS-Port-Type G.3-Fax 10 129 | VALUE NAS-Port-Type SDSL 11 130 | VALUE NAS-Port-Type ADSL-CAP 12 131 | VALUE NAS-Port-Type ADSL-DMT 13 132 | VALUE NAS-Port-Type IDSL 14 133 | VALUE NAS-Port-Type Ethernet 15 134 | VALUE NAS-Port-Type xDSL 16 135 | VALUE NAS-Port-Type Cable 17 136 | VALUE NAS-Port-Type Wireless-Other 18 137 | VALUE NAS-Port-Type Wireless-802.11 19 138 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc2866: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | # 3 | # Attributes and values defined in RFC 2866. 4 | # http://www.ietf.org/rfc/rfc2866.txt 5 | # 6 | # $Id: dictionary.rfc2866 39598 2011-10-26 05:33:30Z etxrab $ 7 | # 8 | ATTRIBUTE Acct-Status-Type 40 integer 9 | ATTRIBUTE Acct-Delay-Time 41 integer 10 | ATTRIBUTE Acct-Input-Octets 42 integer 11 | ATTRIBUTE Acct-Output-Octets 43 integer 12 | ATTRIBUTE Acct-Session-Id 44 string 13 | ATTRIBUTE Acct-Authentic 45 integer 14 | ATTRIBUTE Acct-Session-Time 46 integer 15 | ATTRIBUTE Acct-Input-Packets 47 integer 16 | ATTRIBUTE Acct-Output-Packets 48 integer 17 | ATTRIBUTE Acct-Terminate-Cause 49 integer 18 | ATTRIBUTE Acct-Multi-Session-Id 50 string 19 | ATTRIBUTE Acct-Link-Count 51 integer 20 | 21 | # Accounting Status Types 22 | 23 | VALUE Acct-Status-Type Start 1 24 | VALUE Acct-Status-Type Stop 2 25 | VALUE Acct-Status-Type Interim-Update 3 26 | VALUE Acct-Status-Type Accounting-On 7 27 | VALUE Acct-Status-Type Accounting-Off 8 28 | VALUE Acct-Status-Type Failed 15 29 | 30 | # Authentication Types 31 | 32 | VALUE Acct-Authentic RADIUS 1 33 | VALUE Acct-Authentic Local 2 34 | VALUE Acct-Authentic Remote 3 35 | VALUE Acct-Authentic Diameter 4 36 | 37 | # Acct Terminate Causes 38 | 39 | VALUE Acct-Terminate-Cause User-Request 1 40 | VALUE Acct-Terminate-Cause Lost-Carrier 2 41 | VALUE Acct-Terminate-Cause Lost-Service 3 42 | VALUE Acct-Terminate-Cause Idle-Timeout 4 43 | VALUE Acct-Terminate-Cause Session-Timeout 5 44 | VALUE Acct-Terminate-Cause Admin-Reset 6 45 | VALUE Acct-Terminate-Cause Admin-Reboot 7 46 | VALUE Acct-Terminate-Cause Port-Error 8 47 | VALUE Acct-Terminate-Cause NAS-Error 9 48 | VALUE Acct-Terminate-Cause NAS-Request 10 49 | VALUE Acct-Terminate-Cause NAS-Reboot 11 50 | VALUE Acct-Terminate-Cause Port-Unneeded 12 51 | VALUE Acct-Terminate-Cause Port-Preempted 13 52 | VALUE Acct-Terminate-Cause Port-Suspended 14 53 | VALUE Acct-Terminate-Cause Service-Unavailable 15 54 | VALUE Acct-Terminate-Cause Callback 16 55 | VALUE Acct-Terminate-Cause User-Error 17 56 | VALUE Acct-Terminate-Cause Host-Request 18 57 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc2867: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | # 3 | # Attributes and values defined in RFC 2867. 4 | # http://www.ietf.org/rfc/rfc2867.txt 5 | # 6 | # $Id: dictionary.rfc2867 28946 2009-07-06 12:39:58Z wmeier $ 7 | # 8 | ATTRIBUTE Acct-Tunnel-Connection 68 string 9 | ATTRIBUTE Acct-Tunnel-Packets-Lost 86 integer 10 | 11 | VALUE Acct-Status-Type Tunnel-Start 9 12 | VALUE Acct-Status-Type Tunnel-Stop 10 13 | VALUE Acct-Status-Type Tunnel-Reject 11 14 | VALUE Acct-Status-Type Tunnel-Link-Start 12 15 | VALUE Acct-Status-Type Tunnel-Link-Stop 13 16 | VALUE Acct-Status-Type Tunnel-Link-Reject 14 17 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc2868: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | # 3 | # Attributes and values defined in RFC 2868. 4 | # http://www.ietf.org/rfc/rfc2868.txt 5 | # 6 | # $Id: dictionary.rfc2868 28946 2009-07-06 12:39:58Z wmeier $ 7 | # 8 | ATTRIBUTE Tunnel-Type 64 integer has_tag 9 | ATTRIBUTE Tunnel-Medium-Type 65 integer has_tag 10 | ATTRIBUTE Tunnel-Client-Endpoint 66 string has_tag 11 | ATTRIBUTE Tunnel-Server-Endpoint 67 string has_tag 12 | 13 | ATTRIBUTE Tunnel-Password 69 string has_tag,encrypt=2 14 | 15 | ATTRIBUTE Tunnel-Private-Group-Id 81 string has_tag 16 | ATTRIBUTE Tunnel-Assignment-Id 82 string has_tag 17 | ATTRIBUTE Tunnel-Preference 83 integer has_tag 18 | 19 | ATTRIBUTE Tunnel-Client-Auth-Id 90 string has_tag 20 | ATTRIBUTE Tunnel-Server-Auth-Id 91 string has_tag 21 | 22 | # Tunnel Type 23 | 24 | VALUE Tunnel-Type PPTP 1 25 | VALUE Tunnel-Type L2F 2 26 | VALUE Tunnel-Type L2TP 3 27 | VALUE Tunnel-Type ATMP 4 28 | VALUE Tunnel-Type VTP 5 29 | VALUE Tunnel-Type AH 6 30 | VALUE Tunnel-Type IP 7 31 | VALUE Tunnel-Type MIN-IP 8 32 | VALUE Tunnel-Type ESP 9 33 | VALUE Tunnel-Type GRE 10 34 | VALUE Tunnel-Type DVS 11 35 | VALUE Tunnel-Type IP-in-IP 12 36 | 37 | # Tunnel Medium Type 38 | 39 | VALUE Tunnel-Medium-Type IP 1 40 | VALUE Tunnel-Medium-Type IPv4 1 41 | VALUE Tunnel-Medium-Type IPv6 2 42 | VALUE Tunnel-Medium-Type NSAP 3 43 | VALUE Tunnel-Medium-Type HDLC 4 44 | VALUE Tunnel-Medium-Type BBN-1822 5 45 | VALUE Tunnel-Medium-Type IEEE-802 6 46 | VALUE Tunnel-Medium-Type E.163 7 47 | VALUE Tunnel-Medium-Type E.164 8 48 | VALUE Tunnel-Medium-Type F.69 9 49 | VALUE Tunnel-Medium-Type X.121 10 50 | VALUE Tunnel-Medium-Type IPX 11 51 | VALUE Tunnel-Medium-Type Appletalk 12 52 | VALUE Tunnel-Medium-Type DecNet-IV 13 53 | VALUE Tunnel-Medium-Type Banyan-Vines 14 54 | VALUE Tunnel-Medium-Type E.164-NSAP 15 55 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc2869: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | # 3 | # Attributes and values defined in RFC 2869. 4 | # http://www.ietf.org/rfc/rfc2869.txt 5 | # 6 | # $Id: dictionary.rfc2869 28946 2009-07-06 12:39:58Z wmeier $ 7 | # 8 | ATTRIBUTE Acct-Input-Gigawords 52 integer 9 | ATTRIBUTE Acct-Output-Gigawords 53 integer 10 | 11 | ATTRIBUTE Event-Timestamp 55 date 12 | 13 | ATTRIBUTE ARAP-Password 70 octets # 16 octets of data 14 | ATTRIBUTE ARAP-Features 71 octets # 14 octets of data 15 | ATTRIBUTE ARAP-Zone-Access 72 integer 16 | ATTRIBUTE ARAP-Security 73 integer 17 | ATTRIBUTE ARAP-Security-Data 74 string 18 | ATTRIBUTE Password-Retry 75 integer 19 | ATTRIBUTE Prompt 76 integer 20 | ATTRIBUTE Connect-Info 77 string 21 | ATTRIBUTE Configuration-Token 78 string 22 | ATTRIBUTE EAP-Message 79 octets 23 | ATTRIBUTE Message-Authenticator 80 octets 24 | 25 | ATTRIBUTE ARAP-Challenge-Response 84 octets # 8 octets of data 26 | ATTRIBUTE Acct-Interim-Interval 85 integer 27 | # 86: RFC 2867 28 | ATTRIBUTE NAS-Port-Id 87 string 29 | ATTRIBUTE Framed-Pool 88 string 30 | 31 | # ARAP Zone Access 32 | 33 | VALUE ARAP-Zone-Access Default-Zone 1 34 | VALUE ARAP-Zone-Access Zone-Filter-Inclusive 2 35 | VALUE ARAP-Zone-Access Zone-Filter-Exclusive 4 36 | 37 | # Prompt 38 | VALUE Prompt No-Echo 0 39 | VALUE Prompt Echo 1 40 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc3162: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | # 3 | # Attributes and values defined in RFC 3162. 4 | # http://www.ietf.org/rfc/rfc3162.txt 5 | # 6 | # $Id: dictionary.rfc3162 28946 2009-07-06 12:39:58Z wmeier $ 7 | # 8 | ATTRIBUTE NAS-IPv6-Address 95 ipv6addr 9 | ATTRIBUTE Framed-Interface-Id 96 ifid 10 | ATTRIBUTE Framed-IPv6-Prefix 97 ipv6prefix 11 | ATTRIBUTE Login-IPv6-Host 98 ipv6addr 12 | ATTRIBUTE Framed-IPv6-Route 99 string 13 | ATTRIBUTE Framed-IPv6-Pool 100 string 14 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc3576: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | # 3 | # Attributes and values defined in RFC 3576. 4 | # http://www.ietf.org/rfc/rfc3576.txt 5 | # 6 | # $Id: dictionary.rfc3576 28946 2009-07-06 12:39:58Z wmeier $ 7 | # 8 | ATTRIBUTE Error-Cause 101 integer 9 | 10 | # Service Types 11 | 12 | VALUE Service-Type Authorize-Only 17 13 | 14 | # Error causes 15 | 16 | VALUE Error-Cause Residual-Context-Removed 201 17 | VALUE Error-Cause Invalid-EAP-Packet 202 18 | VALUE Error-Cause Unsupported-Attribute 401 19 | VALUE Error-Cause Missing-Attribute 402 20 | VALUE Error-Cause NAS-Identification-Mismatch 403 21 | VALUE Error-Cause Invalid-Request 404 22 | VALUE Error-Cause Unsupported-Service 405 23 | VALUE Error-Cause Unsupported-Extension 406 24 | VALUE Error-Cause Administratively-Prohibited 501 25 | VALUE Error-Cause Proxy-Request-Not-Routable 502 26 | VALUE Error-Cause Session-Context-Not-Found 503 27 | VALUE Error-Cause Session-Context-Not-Removable 504 28 | VALUE Error-Cause Proxy-Processing-Error 505 29 | VALUE Error-Cause Resources-Unavailable 506 30 | VALUE Error-Cause Request-Initiated 507 31 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc3580: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | # 3 | # Attributes and values defined in RFC 3580. 4 | # http://www.ietf.org/rfc/rfc3580.txt 5 | # 6 | # $Id: dictionary.rfc3580 28946 2009-07-06 12:39:58Z wmeier $ 7 | # 8 | VALUE Acct-Terminate-Cause Supplicant-Restart 19 9 | VALUE Acct-Terminate-Cause Reauthentication-Failure 20 10 | VALUE Acct-Terminate-Cause Port-Reinit 21 11 | VALUE Acct-Terminate-Cause Port-Disabled 22 12 | 13 | VALUE NAS-Port-Type Token-Ring 20 14 | VALUE NAS-Port-Type FDDI 21 15 | 16 | VALUE Tunnel-Type VLAN 13 17 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc4072: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | # 3 | # Attributes and values defined in RFC 4072 4 | # http://www.ietf.org/rfc/4072.txt 5 | # 6 | # $Id: dictionary.rfc4072 28946 2009-07-06 12:39:58Z wmeier $ 7 | # 8 | 9 | ATTRIBUTE EAP-Key-Name 102 string 10 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc4372: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | # 3 | # Attributes and values defined in RFC 4372. 4 | # http://www.ietf.org/rfc/4372.txt 5 | # 6 | # $Id: dictionary.rfc4372 28946 2009-07-06 12:39:58Z wmeier $ 7 | # 8 | ATTRIBUTE Chargeable-User-Identity 89 string 9 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc4603: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | ############################################################################## 3 | # 4 | # Attributes and values defined in RFC 4603. 5 | # http://www.ietf.org/rfc/rfc4603.txt 6 | # 7 | # $Id: dictionary.rfc4603 39598 2011-10-26 05:33:30Z etxrab $ 8 | # 9 | ############################################################################## 10 | 11 | 12 | VALUE NAS-Port-Type PPPoA 30 13 | VALUE NAS-Port-Type PPPoEoA 31 14 | VALUE NAS-Port-Type PPPoEoE 32 15 | VALUE NAS-Port-Type PPPoEoVLAN 33 16 | VALUE NAS-Port-Type PPPoEoQinQ 34 17 | 18 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc4675: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | # 3 | # Attributes and values defined in RFC 4675. 4 | # http://www.ietf.org/rfc/4675.txt 5 | # 6 | # $Id: dictionary.rfc4675 28946 2009-07-06 12:39:58Z wmeier $ 7 | # 8 | 9 | # 10 | # High byte = '1' (0x31) means the frames are tagged. 11 | # High byte = '2' (0x32) means the frames are untagged. 12 | # 13 | # Next 12 bits MUST be zero. 14 | # 15 | # Lower 12 bits is the IEEE-802.1Q VLAN VID. 16 | # 17 | ATTRIBUTE Egress-VLANID 56 integer 18 | ATTRIBUTE Ingress-Filters 57 integer 19 | 20 | # 21 | # First byte == '1' (0x31) means that the frames are tagged. 22 | # First byte == '2' (0x32) means that the frames are untagged. 23 | # 24 | ATTRIBUTE Egress-VLAN-Name 58 string 25 | ATTRIBUTE User-Priority-Table 59 octets # 8 26 | 27 | VALUE Ingress-Filters Enabled 1 28 | VALUE Ingress-Filters Disabled 2 29 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc4679: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | # 3 | # Attributes and values defined in RFC 4679. 4 | # http://www.ietf.org/rfc/4679.txt 5 | # 6 | # $Id: dictionary.rfc4679 28946 2009-07-06 12:39:58Z wmeier $ 7 | # 8 | 9 | VENDOR ADSL-Forum 3561 10 | 11 | BEGIN-VENDOR ADSL-Forum 12 | 13 | # 14 | # The first two attributes are prefixed with "ADSL-" because of 15 | # conflicting names in dictionary.redback. 16 | # 17 | ATTRIBUTE ADSL-Agent-Circuit-Id 1 string 18 | ATTRIBUTE ADSL-Agent-Remote-Id 2 string 19 | ATTRIBUTE Actual-Data-Rate-Upstream 129 integer 20 | ATTRIBUTE Actual-Data-Rate-Downstream 130 integer 21 | ATTRIBUTE Minimum-Data-Rate-Upstream 131 integer 22 | ATTRIBUTE Minimum-Data-Rate-Downstream 132 integer 23 | ATTRIBUTE Attainable-Data-Rate-Upstream 133 integer 24 | ATTRIBUTE Attainable-Data-Rate-Downstream 134 integer 25 | ATTRIBUTE Maximum-Data-Rate-Upstream 135 integer 26 | ATTRIBUTE Maximum-Data-Rate-Downstream 136 integer 27 | ATTRIBUTE Minimum-Data-Rate-Upstream-Low-Power 137 integer 28 | ATTRIBUTE Minimum-Data-Rate-Downstream-Low-Power 138 integer 29 | ATTRIBUTE Maximum-Interleaving-Delay-Upstream 139 integer 30 | ATTRIBUTE Actual-Interleaving-Delay-Upstream 140 integer 31 | ATTRIBUTE Maximum-Interleaving-Delay-Downstream 141 integer 32 | ATTRIBUTE Actual-Interleaving-Delay-Downstream 142 integer 33 | 34 | # 35 | # This next attribute has a weird encoding. 36 | # 37 | # Octet[0] - 0x01 AAL5 38 | # Octet[0] - 0x02 Ethernet 39 | 40 | # Octet[1] - 0x00 Not Available 41 | # Octet[1] - 0x01 Untagged Ethernet 42 | # Octet[1] - 0x02 Single-Tagged Ethernet 43 | 44 | # Octet[2] - 0x00 Not available 45 | # Octet[2] - 0x01 PPPoA LLC 46 | # Octet[2] - 0x02 PPPoA Null 47 | # Octet[2] - 0x03 IPoA LLC 48 | # Octet[2] - 0x04 IPoA NULL 49 | # Octet[2] - 0x05 Ethernet over AAL5 LLC with FCS 50 | # Octet[2] - 0x06 Ethernet over AAL5 LLC without FCS 51 | # Octet[2] - 0x07 Ethernet over AAL5 Null with FCS 52 | # Octet[2] - 0x08 Ethernet over AAL5 Null without FCS 53 | # 54 | ATTRIBUTE Access-Loop-Encapsulation 144 octets # 3 55 | 56 | # 57 | # If this attribute exists, it means that IFW has been performed 58 | # for the subscribers session. 59 | # 60 | ATTRIBUTE IWF-Session 252 octets # 0 61 | 62 | END-VENDOR ADSL-Forum 63 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc4818: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | ############################################################################## 3 | # 4 | # Attributes and values defined in RFC 4818. 5 | # http://www.ietf.org/rfc/rfc4818.txt 6 | # 7 | # $Id: dictionary.rfc4818 28946 2009-07-06 12:39:58Z wmeier $ 8 | # 9 | ############################################################################## 10 | 11 | ATTRIBUTE Delegated-IPv6-Prefix 123 ipv6prefix 12 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc4849: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | # 3 | # Attributes and values defined in RFC 4849. 4 | # http://www.ietf.org/rfc/rfc4849.txt 5 | # 6 | # $Id: dictionary.rfc4849 28946 2009-07-06 12:39:58Z wmeier $ 7 | # 8 | ATTRIBUTE NAS-Filter-Rule 92 string 9 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc5090: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | # 3 | # Attributes and values defined in RFC 5090. 4 | # http://www.ietf.org/rfc/rfc5090.txt 5 | # 6 | # $Id: dictionary.rfc5090 28946 2009-07-06 12:39:58Z wmeier $ 7 | # 8 | ATTRIBUTE Digest-Response 103 string 9 | ATTRIBUTE Digest-Realm 104 string 10 | ATTRIBUTE Digest-Nonce 105 string 11 | ATTRIBUTE Digest-Response-Auth 106 string 12 | ATTRIBUTE Digest-Nextnonce 107 string 13 | ATTRIBUTE Digest-Method 108 string 14 | ATTRIBUTE Digest-URI 109 string 15 | ATTRIBUTE Digest-Qop 110 string 16 | ATTRIBUTE Digest-Algorithm 111 string 17 | ATTRIBUTE Digest-Entity-Body-Hash 112 string 18 | ATTRIBUTE Digest-CNonce 113 string 19 | ATTRIBUTE Digest-Nonce-Count 114 string 20 | ATTRIBUTE Digest-Username 115 string 21 | ATTRIBUTE Digest-Opaque 116 string 22 | ATTRIBUTE Digest-Auth-Param 117 string 23 | ATTRIBUTE Digest-AKA-Auts 118 string 24 | ATTRIBUTE Digest-Domain 119 string 25 | ATTRIBUTE Digest-Stale 120 string 26 | ATTRIBUTE Digest-HA1 121 string 27 | ATTRIBUTE SIP-AOR 122 string 28 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc5176: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | # 3 | # Attributes and values defined in RFC 5176. 4 | # http://www.ietf.org/rfc/rfc5176.txt 5 | # 6 | # $Id: dictionary.rfc5176 28946 2009-07-06 12:39:58Z wmeier $ 7 | # 8 | VALUE Error-Cause Invalid-Attribute-Value 407 9 | VALUE Error-Cause Multiple-Session-Selection-Unsupported 508 10 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc5580: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | # 3 | # Attributes and values defined in RFC 5580. 4 | # http://www.ietf.org/rfc/rfc5580.txt 5 | # 6 | # $Id: dictionary.rfc5580 39598 2011-10-26 05:33:30Z etxrab $ 7 | # 8 | 9 | # One ASCII character of Namespace ID 10 | # 0 = TADIG (GSM) 11 | # 1 = Realm 12 | # 2 = E212 13 | # 14 | # 15 | # Followed by the actual string 16 | ATTRIBUTE Operator-Name 126 string 17 | 18 | # 19 | # Large blobs of stuff 20 | # 21 | ATTRIBUTE Location-Information 127 octets 22 | ATTRIBUTE Location-Data 128 octets 23 | ATTRIBUTE Basic-Location-Policy-Rules 129 octets 24 | ATTRIBUTE Extended-Location-Policy-Rules 130 octets 25 | 26 | # 27 | # Really a bit-packed field 28 | # 29 | ATTRIBUTE Location-Capable 131 integer 30 | VALUE Location-Capable Civix-Location 1 31 | VALUE Location-Capable Geo-Location 2 32 | VALUE Location-Capable Users-Location 4 33 | VALUE Location-Capable NAS-Location 8 34 | 35 | ATTRIBUTE Requested-Location-Info 132 integer 36 | VALUE Requested-Location-Info Civix-Location 1 37 | VALUE Requested-Location-Info Geo-Location 2 38 | VALUE Requested-Location-Info Users-Location 4 39 | VALUE Requested-Location-Info NAS-Location 8 40 | VALUE Requested-Location-Info Future-Requests 16 41 | VALUE Requested-Location-Info None 32 42 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc5607: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | # 3 | # Attributes and values defined in RFC 5607. 4 | # http://www.ietf.org/rfc/rfc5607.txt 5 | # 6 | # $Id: dictionary.rfc5607 39598 2011-10-26 05:33:30Z etxrab $ 7 | # 8 | 9 | VALUE Service-Type Framed-Management 18 10 | 11 | ATTRIBUTE Framed-Management 133 integer 12 | 13 | VALUE Framed-Management SNMP 1 14 | VALUE Framed-Management Web-Based 2 15 | VALUE Framed-Management Netconf 3 16 | VALUE Framed-Management FTP 4 17 | VALUE Framed-Management TFTP 5 18 | VALUE Framed-Management SFTP 6 19 | VALUE Framed-Management RCP 7 20 | VALUE Framed-Management SCP 8 21 | 22 | ATTRIBUTE Management-Transport-Protection 134 integer 23 | 24 | VALUE Management-Transport-Protection No-Protection 1 25 | VALUE Management-Transport-Protection Integrity-Protection 2 26 | VALUE Management-Transport-Protection Integrity-Confidentiality-Protection 3 27 | 28 | ATTRIBUTE Management-Policy-Id 135 string 29 | 30 | ATTRIBUTE Management-Privilege-Level 136 integer 31 | -------------------------------------------------------------------------------- /dictionaries/dictionary.rfc5904: -------------------------------------------------------------------------------- 1 | # -*- text -*- 2 | # 3 | # Attributes and values defined in RFC 5904. 4 | # http://www.ietf.org/rfc/rfc5904.txt 5 | # 6 | # $Id: dictionary.rfc5904 39598 2011-10-26 05:33:30Z etxrab $ 7 | # 8 | 9 | # The next two attributes are continued, like EAP-Message/ 10 | ATTRIBUTE PKM-SS-Cert 137 octets 11 | ATTRIBUTE PKM-CA-Cert 138 octets 12 | 13 | # 28 bytes of data, 7 integers 14 | ATTRIBUTE PKM-Config-Settings 139 octets 15 | ATTRIBUTE PKM-Cryptosuite-List 140 octets 16 | ATTRIBUTE PKM-SAID 141 short 17 | 18 | # 6 bytes of data: SAID, 1 byte of type, 3 of cryptosuite 19 | ATTRIBUTE PKM-SA-Descriptor 142 octets 20 | 21 | # 133 bytes of data: integer lifetime, 1 byte sequence, 128 bytes of key 22 | ATTRIBUTE PKM-Auth-Key 143 octets 23 | -------------------------------------------------------------------------------- /examples/auth_client.js: -------------------------------------------------------------------------------- 1 | // Example radius client sending auth packets. 2 | 3 | var radius = require('../lib/radius'); 4 | var dgram = require('dgram'); 5 | var util = require('util'); 6 | 7 | var secret = 'radius_secret'; 8 | 9 | var packet_accepted = { 10 | code: "Access-Request", 11 | secret: secret, 12 | identifier: 0, 13 | attributes: [ 14 | ['NAS-IP-Address', '10.5.5.5'], 15 | ['User-Name', 'jlpicard'], 16 | ['User-Password', 'beverly123'] 17 | ] 18 | }; 19 | 20 | var packet_rejected = { 21 | code: "Access-Request", 22 | secret: secret, 23 | identifier: 1, 24 | attributes: [ 25 | ['NAS-IP-Address', '10.5.5.5'], 26 | ['User-Name', 'egarak'], 27 | ['User-Password', 'tailoredfit'] 28 | ] 29 | }; 30 | 31 | var packet_wrong_secret = { 32 | code: "Access-Request", 33 | secret: "wrong_secret", 34 | identifier: 2, 35 | attributes: [ 36 | ['NAS-IP-Address', '10.5.5.5'], 37 | ['User-Name', 'riker'], 38 | ['User-Password', 'Riker-Omega-3'] 39 | ] 40 | }; 41 | 42 | var client = dgram.createSocket("udp4"); 43 | 44 | client.bind(49001); 45 | 46 | var response_count = 0; 47 | 48 | client.on('message', function(msg, rinfo) { 49 | var response = radius.decode({packet: msg, secret: secret}); 50 | var request = sent_packets[response.identifier]; 51 | 52 | // although it's a slight hassle to keep track of packets, it's a good idea to verify 53 | // responses to make sure you are talking to a server with the same shared secret 54 | var valid_response = radius.verify_response({ 55 | response: msg, 56 | request: request.raw_packet, 57 | secret: request.secret 58 | }); 59 | if (valid_response) { 60 | console.log('Got valid response ' + response.code + ' for packet id ' + response.identifier); 61 | // take some action based on response.code 62 | } else { 63 | console.log('WARNING: Got invalid response ' + response.code + ' for packet id ' + response.identifier); 64 | // don't take action since server cannot be trusted (but maybe alert user that shared secret may be incorrect) 65 | } 66 | 67 | if (++response_count == 3) { 68 | client.close(); 69 | } 70 | }); 71 | 72 | var sent_packets = {}; 73 | 74 | [packet_accepted, packet_rejected, packet_wrong_secret].forEach(function(packet) { 75 | var encoded = radius.encode(packet); 76 | sent_packets[packet.identifier] = { 77 | raw_packet: encoded, 78 | secret: packet.secret 79 | }; 80 | client.send(encoded, 0, encoded.length, 1812, "localhost"); 81 | }); 82 | -------------------------------------------------------------------------------- /examples/auth_server.js: -------------------------------------------------------------------------------- 1 | // Example radius server doing authentication 2 | 3 | var radius = require('../lib/radius'); 4 | var dgram = require("dgram"); 5 | 6 | var secret = 'radius_secret'; 7 | var server = dgram.createSocket("udp4"); 8 | 9 | server.on("message", function (msg, rinfo) { 10 | var code, username, password, packet; 11 | try { 12 | packet = radius.decode({packet: msg, secret: secret}); 13 | } catch (e) { 14 | console.log("Failed to decode radius packet, silently dropping:", e); 15 | return; 16 | } 17 | 18 | if (packet.code != 'Access-Request') { 19 | console.log('unknown packet type: ', packet.code); 20 | return; 21 | } 22 | 23 | username = packet.attributes['User-Name']; 24 | password = packet.attributes['User-Password']; 25 | 26 | console.log('Access-Request for ' + username); 27 | 28 | if (username == 'jlpicard' && password == 'beverly123') { 29 | code = 'Access-Accept'; 30 | } else { 31 | code = 'Access-Reject'; 32 | } 33 | 34 | var response = radius.encode_response({ 35 | packet: packet, 36 | code: code, 37 | secret: secret 38 | }); 39 | 40 | console.log('Sending ' + code + ' for user ' + username); 41 | server.send(response, 0, response.length, rinfo.port, rinfo.address, function(err, bytes) { 42 | if (err) { 43 | console.log('Error sending response to ', rinfo); 44 | } 45 | }); 46 | }); 47 | 48 | server.on("listening", function () { 49 | var address = server.address(); 50 | console.log("radius server listening " + 51 | address.address + ":" + address.port); 52 | }); 53 | 54 | server.bind(1812); 55 | -------------------------------------------------------------------------------- /lib/radius.js: -------------------------------------------------------------------------------- 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 | const MESSAGE_AUTHENTICATOR_LENGTH = 16; 30 | 31 | Radius.InvalidSecretError = function(msg, decoded, constr) { 32 | Error.captureStackTrace(this, constr || this); 33 | this.message = msg || 'Error'; 34 | this.decoded = decoded; 35 | }; 36 | util.inherits(Radius.InvalidSecretError, Error); 37 | Radius.InvalidSecretError.prototype.name = 'Invalid Secret Error'; 38 | 39 | Radius.add_dictionary = function(file) { 40 | dictionary_locations.push(path.resolve(file)); 41 | }; 42 | 43 | var load_dictionaries_cbs = []; 44 | Radius.load_dictionaries = function() { 45 | var self = this; 46 | 47 | if (dictionaries_state == LOADED) { 48 | return; 49 | } 50 | 51 | dictionary_locations.forEach(function(file) { 52 | if (!fs.existsSync(file)) { 53 | throw new Error("Invalid dictionary location: " + file); 54 | } 55 | 56 | if (fs.statSync(file).isDirectory()) { 57 | var files = fs.readdirSync(file); 58 | for (var j = 0; j < files.length; j++) { 59 | self.load_dictionary(file + "/" + files[j]); 60 | } 61 | } else { 62 | self.load_dictionary(file); 63 | } 64 | }); 65 | 66 | dictionaries_state = LOADED; 67 | }; 68 | 69 | Radius.load_dictionary = function(file, seen_files) { 70 | file = path.normalize(file); 71 | var self = this; 72 | 73 | if (seen_files === undefined) { 74 | seen_files = {}; 75 | } 76 | 77 | if (seen_files[file]) { 78 | return; 79 | } 80 | 81 | seen_files[file] = true; 82 | 83 | var includes = self._load_dictionary(fs.readFileSync(file, "ascii")); 84 | includes.forEach(function (i) { 85 | self.load_dictionary(path.join(path.dirname(file), i), seen_files); 86 | }); 87 | }; 88 | 89 | Radius._load_dictionary = function(content) { 90 | var lines = content.split("\n"); 91 | 92 | var vendor = NO_VENDOR, includes = [], attr_vendor; 93 | for (var i = 0; i < lines.length; i++) { 94 | var line = lines[i]; 95 | 96 | line = line.replace(/#.*/, "").replace(/\s+/g, " "); 97 | 98 | var match = line.match(/^\s*VENDOR\s+(\S+)\s+(\d+)/); 99 | if (match) { 100 | vendor_name_to_id[match[1]] = match[2]; 101 | continue; 102 | } 103 | 104 | if ((match = line.match(/^\s*BEGIN-VENDOR\s+(\S+)/))) { 105 | vendor = vendor_name_to_id[match[1]]; 106 | continue; 107 | } 108 | 109 | if (line.match(/^\s*END-VENDOR/)) { 110 | vendor = NO_VENDOR; 111 | continue; 112 | } 113 | 114 | var init_entry = function(vendor, attr_id) { 115 | if (!attributes_map[vendor]) { 116 | attributes_map[vendor] = {}; 117 | } 118 | 119 | if (!attributes_map[vendor][attr_id]) { 120 | attributes_map[vendor][attr_id] = [null, null, null, {}, {}, {}]; 121 | } 122 | }; 123 | 124 | match = line.match(/^\s*(?:VENDORATTR\s+(\d+)|ATTRIBUTE)\s+(\S+)\s+(\d+)\s+(\S+)\s*(.+)?/); 125 | if (match) { 126 | attr_vendor = vendor; 127 | if (match[1] !== undefined) { 128 | attr_vendor = match[1]; 129 | } 130 | 131 | var modifiers = {}; 132 | if (match[5] !== undefined) { 133 | match[5].replace(/\s*/g, "").split(",").forEach(function(m) { 134 | modifiers[m] = true; 135 | }); 136 | } 137 | 138 | init_entry(attr_vendor, match[3]); 139 | 140 | attributes_map[attr_vendor][match[3]][ATTR_ID] = match[3]; 141 | attributes_map[attr_vendor][match[3]][ATTR_NAME] = match[2]; 142 | attributes_map[attr_vendor][match[3]][ATTR_TYPE] = match[4].toLowerCase(); 143 | attributes_map[attr_vendor][match[3]][ATTR_MODIFIERS] = modifiers; 144 | 145 | var by_name = attributes_map[attr_vendor][match[2]]; 146 | if (by_name !== undefined) { 147 | var by_index = attributes_map[attr_vendor][match[3]]; 148 | [ATTR_ENUM, ATTR_REVERSE_ENUM].forEach(function(field) { 149 | for (var name in by_name[field]) { 150 | by_index[field][name] = by_name[field][name]; 151 | } 152 | }); 153 | } 154 | attributes_map[attr_vendor][match[2]] = attributes_map[attr_vendor][match[3]]; 155 | 156 | continue; 157 | } 158 | 159 | match = line.match(/^\s*(?:VENDOR)?VALUE\s+(\d+)?\s*(\S+)\s+(\S+)\s+(\d+)/); 160 | if (match) { 161 | attr_vendor = vendor; 162 | if (match[1] !== undefined) { 163 | attr_vendor = match[1]; 164 | } 165 | 166 | init_entry(attr_vendor, match[2]); 167 | 168 | attributes_map[attr_vendor][match[2]][ATTR_ENUM][match[4]] = match[3]; 169 | attributes_map[attr_vendor][match[2]][ATTR_REVERSE_ENUM][match[3]] = match[4]; 170 | 171 | continue; 172 | } 173 | 174 | if ((match = line.match(/^\s*\$INCLUDE\s+(.*)/))) { 175 | includes.push(match[1]); 176 | } 177 | } 178 | 179 | return includes; 180 | }; 181 | 182 | Radius.unload_dictionaries = function() { 183 | attributes_map = {}; 184 | vendor_name_to_id = {}; 185 | dictionaries_state = NOT_LOADED; 186 | }; 187 | 188 | Radius.attr_name_to_id = function(attr_name, vendor_id) { 189 | return this._attr_to(attr_name, vendor_id, ATTR_ID); 190 | }; 191 | 192 | Radius.attr_id_to_name = function(attr_name, vendor_id) { 193 | return this._attr_to(attr_name, vendor_id, ATTR_NAME); 194 | }; 195 | 196 | Radius.vendor_name_to_id = function(vendor_name) { 197 | return vendor_name_to_id[vendor_name]; 198 | }; 199 | 200 | Radius._attr_to = function(attr, vendor_id, target) { 201 | if (vendor_id === undefined) { 202 | vendor_id = NO_VENDOR; 203 | } 204 | 205 | if (!attributes_map[vendor_id]) { 206 | return; 207 | } 208 | 209 | var attr_info = attributes_map[vendor_id][attr]; 210 | if (!attr_info) { 211 | return; 212 | } 213 | 214 | return attr_info[target]; 215 | }; 216 | 217 | var code_map = { 218 | 1: "Access-Request", 219 | 2: "Access-Accept", 220 | 3: "Access-Reject", 221 | 4: "Accounting-Request", 222 | 5: "Accounting-Response", 223 | 6: "Interim-Accounting", 224 | 7: "Password-Request", 225 | 8: "Password-Ack", 226 | 9: "Password-Reject", 227 | 10: "Accounting-Message", 228 | 11: "Access-Challenge", 229 | 12: "Status-Server", 230 | 13: "Status-Client", 231 | 21: "Resource-Free-Request", 232 | 22: "Resource-Free-Response", 233 | 23: "Resource-Query-Request", 234 | 24: "Resource-Query-Response", 235 | 25: "Alternate-Resource-Reclaim-Request", 236 | 26: "NAS-Reboot-Request", 237 | 27: "NAS-Reboot-Response", 238 | 29: "Next-Passcode", 239 | 30: "New-Pin", 240 | 31: "Terminate-Session", 241 | 32: "Password-Expired", 242 | 33: "Event-Request", 243 | 34: "Event-Response", 244 | 40: "Disconnect-Request", 245 | 41: "Disconnect-ACK", 246 | 42: "Disconnect-NAK", 247 | 43: "CoA-Request", 248 | 44: "CoA-ACK", 249 | 45: "CoA-NAK", 250 | 50: "IP-Address-Allocate", 251 | 51: "IP-Address-Release" 252 | }; 253 | 254 | var uses_random_authenticator = { 255 | "Access-Request": true, 256 | "Status-Server": true 257 | }; 258 | 259 | var is_request_code = { 260 | "Status-Server": true 261 | }; 262 | 263 | var reverse_code_map = {}; 264 | for (var code in code_map) { 265 | reverse_code_map[code_map[code]] = code; 266 | if (code_map[code].match(/Request/)) { 267 | is_request_code[code_map[code]] = true; 268 | } 269 | } 270 | 271 | Radius.error = function(error_msg) { 272 | var err = error_msg; 273 | if (typeof(error_msg) === 'string') { 274 | err = new Error(error_msg); 275 | } 276 | 277 | throw err; 278 | }; 279 | 280 | // this is a convenience method, "decode({..., no_secret: true})" will also do the job 281 | Radius.decode_without_secret = function(args) { 282 | // copy args' fields without modifiying the orginal 283 | var nargs = {no_secret: true}; 284 | for (var p in args) { 285 | nargs[p] = args[p]; 286 | } 287 | return this.decode(nargs, this._decode); 288 | }; 289 | 290 | Radius.decode = function(args) { 291 | this.load_dictionaries(); 292 | 293 | var packet = args.packet; 294 | if (!packet || packet.length < 4) { 295 | this.error("decode: packet too short"); 296 | return; 297 | } 298 | 299 | var ret = {}; 300 | 301 | ret.code = code_map[packet.readUInt8(0)]; 302 | 303 | if (!ret.code) { 304 | this.error("decode: invalid packet code '" + packet.readUInt8(0) + "'"); 305 | return; 306 | } 307 | 308 | ret.identifier = packet.readUInt8(1); 309 | ret.length = packet.readUInt16BE(2); 310 | 311 | if (packet.length < ret.length) { 312 | this.error("decode: incomplete packet"); 313 | return; 314 | } 315 | 316 | this.authenticator = ret.authenticator = packet.slice(AUTH_START, AUTH_END); 317 | this.no_secret = args.no_secret; 318 | this.secret = args.secret; 319 | 320 | var attrs = packet.slice(AUTH_END, ret.length); 321 | ret.attributes = {}; 322 | ret.raw_attributes = []; 323 | 324 | try { 325 | this.decode_attributes(attrs, ret.attributes, NO_VENDOR, ret.raw_attributes); 326 | } catch(err) { 327 | this.error(err); 328 | return; 329 | } 330 | 331 | if (!uses_random_authenticator[ret.code] && is_request_code[ret.code] && !args.no_secret) { 332 | var orig_authenticator = new Buffer(AUTH_LENGTH); 333 | packet.copy(orig_authenticator, 0, AUTH_START, AUTH_END); 334 | packet.fill(0, AUTH_START, AUTH_END); 335 | 336 | var checksum = this.calculate_packet_checksum(packet, args.secret); 337 | orig_authenticator.copy(packet, AUTH_START); 338 | 339 | if (checksum.toString() != this.authenticator.toString()) { 340 | this.error(new Radius.InvalidSecretError("decode: authenticator mismatch (possible shared secret mismatch)", ret)); 341 | return; 342 | } 343 | } 344 | 345 | if (is_request_code[ret.code] && ret.attributes["Message-Authenticator"] && !args.no_secret) { 346 | this._verify_request_message_authenticator(args, ret); 347 | } 348 | 349 | return ret; 350 | }; 351 | 352 | Radius.zero_out_message_authenticator = function(attributes) { 353 | var ma_id = this.attr_name_to_id("Message-Authenticator"); 354 | var new_attrs = attributes.slice(0); 355 | for (var i = 0; i < new_attrs.length; i++) { 356 | var attr = new_attrs[i]; 357 | if (attr[0] == ma_id) { 358 | new_attrs[i] = [ma_id, new Buffer(MESSAGE_AUTHENTICATOR_LENGTH)]; 359 | new_attrs[i][1].fill(0x00); 360 | break; 361 | } 362 | } 363 | return new_attrs; 364 | }; 365 | 366 | Radius._verify_request_message_authenticator = function(args, request) { 367 | var reencoded = this.encode({ 368 | code: request.code, 369 | attributes: this.zero_out_message_authenticator(request.raw_attributes), 370 | identifier: request.identifier, 371 | secret: args.secret 372 | }); 373 | 374 | request.authenticator.copy(reencoded, AUTH_START); 375 | 376 | var orig_ma = request.attributes["Message-Authenticator"]; 377 | var expected_ma = this.calculate_message_authenticator(reencoded, args.secret); 378 | 379 | if (orig_ma.toString() != expected_ma.toString()) { 380 | this.error(new Radius.InvalidSecretError("decode: Message-Authenticator mismatch (possible shared secret mismatch)", request)); 381 | } 382 | }; 383 | 384 | Radius.verify_response = function(args) { 385 | this.load_dictionaries(); 386 | 387 | if (!args || !Buffer.isBuffer(args.request) || !Buffer.isBuffer(args.response)) { 388 | this.error("verify_response: must provide raw request and response packets"); 389 | return; 390 | } 391 | 392 | if (args.secret == null) { 393 | this.error("verify_response: must specify shared secret"); 394 | return; 395 | } 396 | 397 | // first verify authenticator 398 | var got_checksum = new Buffer(AUTH_LENGTH); 399 | args.response.copy(got_checksum, 0, AUTH_START, AUTH_END); 400 | args.request.copy(args.response, AUTH_START, AUTH_START, AUTH_END); 401 | 402 | var expected_checksum = this.calculate_packet_checksum(args.response, args.secret); 403 | got_checksum.copy(args.response, AUTH_START); 404 | 405 | if (expected_checksum.toString() != args.response.slice(AUTH_START, AUTH_END).toString()) { 406 | return false; 407 | } 408 | 409 | return this._verify_response_message_authenticator(args); 410 | }; 411 | 412 | Radius._verify_response_message_authenticator = function(args) { 413 | var parsed_request = this.decode({ 414 | packet: args.request, 415 | secret: args.secret 416 | }); 417 | 418 | if (parsed_request.attributes["Message-Authenticator"]) { 419 | var parsed_response = this.decode({ 420 | packet: args.response, 421 | secret: args.secret 422 | }); 423 | 424 | var got_ma = parsed_response.attributes["Message-Authenticator"]; 425 | if (!got_ma) { 426 | return false; 427 | } 428 | 429 | var expected_response = this.encode({ 430 | secret: args.secret, 431 | code: parsed_response.code, 432 | identifier: parsed_response.identifier, 433 | attributes: this.zero_out_message_authenticator(parsed_response.raw_attributes) 434 | }); 435 | parsed_request.authenticator.copy(expected_response, AUTH_START); 436 | var expected_ma = this.calculate_message_authenticator(expected_response, args.secret); 437 | if (expected_ma.toString() != got_ma.toString()) { 438 | return false; 439 | } 440 | } 441 | 442 | return true; 443 | }; 444 | 445 | Radius.decode_attributes = function(data, attr_hash, vendor, raw_attrs) { 446 | var type, length, value, tag; 447 | while (data.length > 0) { 448 | type = data.readUInt8(0); 449 | length = data.readUInt8(1); 450 | value = data.slice(2, length); 451 | tag = undefined; 452 | 453 | if (length < 2) { 454 | throw new Error("invalid attribute length: " + length); 455 | } 456 | 457 | if (raw_attrs) { 458 | raw_attrs.push([type, value]); 459 | } 460 | 461 | data = data.slice(length); 462 | var attr_info = attributes_map[vendor] && attributes_map[vendor][type]; 463 | if (!attr_info) { 464 | continue; 465 | } 466 | 467 | if (attr_info[ATTR_MODIFIERS]["has_tag"]) { 468 | var first_byte = value.readUInt8(0); 469 | if (first_byte <= 0x1F) { 470 | tag = first_byte; 471 | value = value.slice(1); 472 | } 473 | } 474 | 475 | if (attr_info[ATTR_MODIFIERS]["encrypt=1"]) { 476 | value = this.decrypt_field(value); 477 | } else { 478 | switch (attr_info[ATTR_TYPE]) { 479 | case "string": 480 | case "text": 481 | // assumes utf8 encoding for strings 482 | value = value.toString("utf8"); 483 | break; 484 | case "ipaddr": 485 | var octets = []; 486 | for (var i = 0; i < value.length; i++) { 487 | octets.push(value[i]); 488 | } 489 | value = octets.join("."); 490 | break; 491 | case "date": 492 | value = new Date(value.readUInt32BE(0) * 1000); 493 | break; 494 | case "time": 495 | case "integer": 496 | if (attr_info[ATTR_MODIFIERS]["has_tag"]) { 497 | var buf = new Buffer([0, 0, 0, 0]); 498 | value.copy(buf, 1); 499 | value = buf; 500 | } 501 | 502 | value = value.readUInt32BE(0); 503 | value = attr_info[ATTR_ENUM][value] || value; 504 | break; 505 | } 506 | 507 | if (attr_info[ATTR_NAME] == "Vendor-Specific") { 508 | if (value[0] !== 0x00) { 509 | throw new Error("Invalid vendor id"); 510 | } 511 | 512 | var vendor_attrs = attr_hash["Vendor-Specific"]; 513 | if (!vendor_attrs) { 514 | vendor_attrs = attr_hash["Vendor-Specific"] = {}; 515 | } 516 | 517 | this.decode_attributes(value.slice(4), vendor_attrs, value.readUInt32BE(0)); 518 | continue; 519 | } 520 | } 521 | 522 | if (tag !== undefined) { 523 | value = [tag, value]; 524 | } 525 | 526 | if (attr_hash[attr_info[ATTR_NAME]] !== undefined) { 527 | if (!(attr_hash[attr_info[ATTR_NAME]] instanceof Array)) { 528 | attr_hash[attr_info[ATTR_NAME]] = [attr_hash[attr_info[ATTR_NAME]]]; 529 | } 530 | 531 | attr_hash[attr_info[ATTR_NAME]].push(value); 532 | } else { 533 | attr_hash[attr_info[ATTR_NAME]] = value; 534 | } 535 | } 536 | }; 537 | 538 | Radius.decrypt_field = function(field) { 539 | if (this.no_secret) { 540 | return null; 541 | } 542 | 543 | if (field.length < 16) { 544 | throw new Error("Invalid password: too short"); 545 | } 546 | 547 | if (field.length > 128) { 548 | throw new Error("Invalid password: too long"); 549 | } 550 | 551 | if (field.length % 16 != 0) { 552 | throw new Error("Invalid password: not padded"); 553 | } 554 | 555 | var decrypted = this._crypt_field(field, true); 556 | if (decrypted === null) return null; 557 | return decrypted.toString("utf8"); 558 | }; 559 | 560 | Radius.encrypt_field = function(field) { 561 | var len = Buffer.byteLength(field, 'utf8'); 562 | var buf = new Buffer(len + 15 - ((15 + len) % 16)); 563 | buf.write(field, 0, len); 564 | 565 | // null-out the padding 566 | for (var i = len; i < buf.length; i++) { 567 | buf[i] = 0x00; 568 | } 569 | 570 | return this._crypt_field(buf, false); 571 | }; 572 | 573 | Radius._crypt_field = function(field, is_decrypt) { 574 | var ret = new Buffer(0); 575 | var second_part_to_be_hashed = this.authenticator; 576 | 577 | if (this.secret === undefined) { 578 | throw new Error("Must provide RADIUS shared secret"); 579 | } 580 | 581 | for (var i = 0; i < field.length; i = i + 16) { 582 | var hasher = crypto.createHash("md5"); 583 | hasher.update(this.secret); 584 | hasher.update(second_part_to_be_hashed); 585 | var hash = new Buffer(hasher.digest("binary"), "binary"); 586 | 587 | var xor_result = new Buffer(16); 588 | for (var j = 0; j < 16; j++) { 589 | xor_result[j] = field[i + j] ^ hash[j]; 590 | if (is_decrypt && xor_result[j] == 0x00) { 591 | xor_result = xor_result.slice(0, j); 592 | break; 593 | } 594 | } 595 | ret = Buffer.concat([ret, xor_result]); 596 | second_part_to_be_hashed = is_decrypt ? field.slice(i, i + 16) : xor_result; 597 | } 598 | 599 | return ret; 600 | }; 601 | 602 | Radius.encode_response = function(args) { 603 | this.load_dictionaries(); 604 | 605 | var packet = args.packet; 606 | if (!packet) { 607 | this.error("encode_response: must provide packet"); 608 | return; 609 | } 610 | 611 | if (!args.attributes) { 612 | args.attributes = []; 613 | } 614 | 615 | var proxy_state_id = this.attr_name_to_id("Proxy-State"); 616 | for (var i = 0; i < packet.raw_attributes.length; i++) { 617 | var attr = packet.raw_attributes[i]; 618 | if (attr[0] == proxy_state_id) { 619 | args.attributes.push(attr); 620 | } 621 | } 622 | 623 | var response = this.encode({ 624 | code: args.code, 625 | identifier: packet.identifier, 626 | authenticator: packet.authenticator, 627 | attributes: args.attributes, 628 | secret: args.secret, 629 | add_message_authenticator: packet.attributes["Message-Authenticator"] != null 630 | }); 631 | 632 | return response; 633 | }; 634 | 635 | Radius.encode = function(args) { 636 | this.load_dictionaries(); 637 | 638 | if (!args || args.code === undefined) { 639 | this.error("encode: must specify code"); 640 | return; 641 | } 642 | 643 | if (args.secret === undefined) { 644 | this.error("encode: must provide RADIUS shared secret"); 645 | return; 646 | } 647 | 648 | var packet = new Buffer(4096); 649 | var offset = 0; 650 | 651 | var code = reverse_code_map[args.code]; 652 | if (code === undefined) { 653 | this.error("encode: invalid packet code '" + args.code + "'"); 654 | return; 655 | } 656 | 657 | packet.writeUInt8(+code, offset++); 658 | 659 | var identifier = args.identifier; 660 | if (identifier === undefined) { 661 | identifier = Math.floor(Math.random() * 256); 662 | } 663 | if (identifier > 255) { 664 | this.error("encode: identifier too large"); 665 | return; 666 | } 667 | packet.writeUInt8(identifier, offset++); 668 | 669 | // save room for length 670 | offset += 2; 671 | 672 | var authenticator = args.authenticator; 673 | 674 | if (!authenticator) { 675 | if (uses_random_authenticator[args.code]) { 676 | authenticator = crypto.randomBytes(AUTH_LENGTH); 677 | } else { 678 | authenticator = new Buffer(AUTH_LENGTH); 679 | authenticator.fill(0x00); 680 | } 681 | } 682 | 683 | return this._encode_with_authenticator(args, packet, offset, authenticator); 684 | }; 685 | 686 | Radius._encode_with_authenticator = function(args, packet, offset, authenticator) { 687 | authenticator.copy(packet, offset); 688 | offset += AUTH_LENGTH; 689 | 690 | this.secret = args.secret; 691 | this.no_secret = false; 692 | this.authenticator = authenticator; 693 | 694 | args.attributes = this.ensure_array_attributes(args.attributes); 695 | 696 | var add_message_authenticator = args.add_message_authenticator; 697 | if (add_message_authenticator == null) { 698 | var eap_id = this.attr_name_to_id("EAP-Message"); 699 | var ma_id = this.attr_name_to_id("Message-Authenticator"); 700 | for (var i = 0; i < args.attributes.length; i++) { 701 | var attr_id = args.attributes[i][0]; 702 | if (attr_id == eap_id || attr_id == "EAP-Message") { 703 | add_message_authenticator = true; 704 | } else if (attr_id == ma_id || attr_id == "Message-Authenticator") { 705 | add_message_authenticator = false; 706 | break; 707 | } 708 | } 709 | if (add_message_authenticator == null && args.code == "Status-Server") { 710 | add_message_authenticator = true; 711 | } 712 | } 713 | 714 | if (add_message_authenticator) { 715 | var empty_authenticator = new Buffer(MESSAGE_AUTHENTICATOR_LENGTH); 716 | empty_authenticator.fill(0x00); 717 | args.attributes.push(["Message-Authenticator", empty_authenticator]); 718 | } 719 | 720 | try { 721 | offset += this.encode_attributes(packet.slice(offset), args.attributes, NO_VENDOR); 722 | } catch (err) { 723 | this.error(err); 724 | return; 725 | } 726 | 727 | // now write the length in 728 | packet.writeUInt16BE(offset, 2); 729 | 730 | packet = packet.slice(0, offset); 731 | 732 | var message_authenticator; 733 | if (add_message_authenticator && !is_request_code[args.code]) { 734 | message_authenticator = this.calculate_message_authenticator(packet, args.secret); 735 | message_authenticator.copy(packet, offset - MESSAGE_AUTHENTICATOR_LENGTH); 736 | } 737 | 738 | if (!uses_random_authenticator[args.code]) { 739 | this.calculate_packet_checksum(packet, args.secret).copy(packet, AUTH_START); 740 | } 741 | 742 | if (add_message_authenticator && is_request_code[args.code]) { 743 | message_authenticator = this.calculate_message_authenticator(packet, args.secret); 744 | message_authenticator.copy(packet, offset - MESSAGE_AUTHENTICATOR_LENGTH); 745 | } 746 | 747 | return packet; 748 | }; 749 | 750 | Radius.calculate_message_authenticator = function(packet, secret) { 751 | var hmac = crypto.createHmac('md5', secret); 752 | hmac.update(packet); 753 | return new Buffer(hmac.digest('binary'), 'binary'); 754 | }; 755 | 756 | Radius.calculate_packet_checksum = function(packet, secret) { 757 | var hasher = crypto.createHash("md5"); 758 | hasher.update(packet); 759 | hasher.update(secret); 760 | return new Buffer(hasher.digest("binary"), "binary"); 761 | }; 762 | 763 | Radius.ensure_array_attributes = function(attributes) { 764 | if (!attributes) { 765 | return []; 766 | } 767 | 768 | if (typeof(attributes) == 'object' && !Array.isArray(attributes)) { 769 | var array_attributes = []; 770 | for (var name in attributes) { 771 | var val = attributes[name]; 772 | if (typeof(val) == 'object') { 773 | throw new Error("Cannot have nested attributes when using hash syntax. Use array syntax instead"); 774 | } 775 | array_attributes.push([name, val]); 776 | } 777 | return array_attributes; 778 | } 779 | 780 | return attributes; 781 | }; 782 | 783 | Radius.encode_attributes = function(packet, attributes, vendor) { 784 | var offset = 0; 785 | for (var i = 0; i < attributes.length; i++) { 786 | var attr = attributes[i]; 787 | var attr_info = attributes_map[vendor] && attributes_map[vendor][attr[0]]; 788 | if (!attr_info && !(attr[1] instanceof Buffer)) { 789 | throw new Error("encode: invalid attributes - must give Buffer for " + 790 | "unknown attribute '" + attr[0] + "'"); 791 | } 792 | 793 | var out_value, in_value = attr[1]; 794 | if (in_value instanceof Buffer) { 795 | out_value = in_value; 796 | } else { 797 | var has_tag = attr_info[ATTR_MODIFIERS]["has_tag"] && attr.length == 3; 798 | 799 | if (has_tag) { 800 | in_value = attr[2]; 801 | } 802 | 803 | if (attr_info[ATTR_MODIFIERS]["encrypt=1"]) { 804 | out_value = this.encrypt_field(in_value); 805 | } else { 806 | switch (attr_info[ATTR_TYPE]) { 807 | case "string": 808 | case "text": 809 | if (in_value.length == 0) { 810 | continue; 811 | } 812 | out_value = new Buffer(in_value + "", "utf8"); 813 | break; 814 | case "ipaddr": 815 | out_value = new Buffer(in_value.split(".")); 816 | if (out_value.length != 4) { 817 | throw new Error("encode: invalid IP: " + in_value); 818 | } 819 | break; 820 | case "date": 821 | in_value = Math.floor(in_value.getTime() / 1000); 822 | case "time": 823 | case "integer": 824 | out_value = new Buffer(4); 825 | 826 | in_value = attr_info[ATTR_REVERSE_ENUM][in_value] || in_value; 827 | if (isNaN(in_value)) { 828 | throw new Error("envode: invalid attribute value: " + in_value); 829 | } 830 | 831 | out_value.writeUInt32BE(+in_value, 0); 832 | 833 | if (has_tag) { 834 | out_value = out_value.slice(1); 835 | } 836 | 837 | break; 838 | default: 839 | if (attr_info[ATTR_NAME] != "Vendor-Specific") { 840 | throw new Error("encode: must provide Buffer for attribute '" + attr_info[ATTR_NAME] + "'"); 841 | } 842 | } 843 | 844 | // handle VSAs specially 845 | if (attr_info[ATTR_NAME] == "Vendor-Specific") { 846 | var vendor_id = isNaN(attr[1]) ? vendor_name_to_id[attr[1]] : attr[1]; 847 | if (vendor_id === undefined) { 848 | throw new Error("encode: unknown vendor '" + attr[1] + "'"); 849 | } 850 | 851 | // write the attribute id 852 | packet.writeUInt8(+attr_info[ATTR_ID], offset++); 853 | 854 | var length = this.encode_attributes(packet.slice(offset + 5), attr[2], vendor_id); 855 | 856 | // write in the length 857 | packet.writeUInt8(2 + 4 + length, offset++); 858 | // write in the vendor id 859 | packet.writeUInt32BE(+vendor_id, offset); 860 | offset += 4; 861 | 862 | offset += length; 863 | continue; 864 | } 865 | } 866 | } 867 | 868 | // write the attribute id 869 | packet.writeUInt8(attr_info ? +attr_info[ATTR_ID] : +attr[0], offset++); 870 | 871 | // write in the attribute length 872 | packet.writeUInt8(2 + out_value.length + (has_tag ? 1 : 0), offset++); 873 | 874 | if (has_tag) { 875 | packet.writeUInt8(attr[1], offset++); 876 | } 877 | 878 | // copy in the attribute value 879 | out_value.copy(packet, offset); 880 | offset += out_value.length; 881 | } 882 | 883 | return offset; 884 | }; 885 | 886 | module.exports = Radius; 887 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "radius", 3 | "version" : "1.1.4", 4 | "description" : "RADIUS packet encoding/decoding", 5 | "author": "Nearbuy Systems ", 6 | "main": "./lib/radius", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/nearbuy/node-radius.git" 10 | }, 11 | "engines": { "node": ">=0.8.0" }, 12 | "devDependencies": { 13 | "nodeunit": "~0.8.6" 14 | }, 15 | "scripts": { 16 | "test": "nodeunit test" 17 | }, 18 | "keywords": ["radius"] 19 | } 20 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /test/captures/aruba_mac_auth.packet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retailnext/node-radius/5b55be28a909359a39c703a262814d0efd9b0bb5/test/captures/aruba_mac_auth.packet -------------------------------------------------------------------------------- /test/captures/cisco_accounting.packet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retailnext/node-radius/5b55be28a909359a39c703a262814d0efd9b0bb5/test/captures/cisco_accounting.packet -------------------------------------------------------------------------------- /test/captures/cisco_accounting_response.packet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retailnext/node-radius/5b55be28a909359a39c703a262814d0efd9b0bb5/test/captures/cisco_accounting_response.packet -------------------------------------------------------------------------------- /test/captures/cisco_mac_auth.packet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retailnext/node-radius/5b55be28a909359a39c703a262814d0efd9b0bb5/test/captures/cisco_mac_auth.packet -------------------------------------------------------------------------------- /test/captures/cisco_mac_auth_reject.packet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retailnext/node-radius/5b55be28a909359a39c703a262814d0efd9b0bb5/test/captures/cisco_mac_auth_reject.packet -------------------------------------------------------------------------------- /test/captures/eap_request.packet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retailnext/node-radius/5b55be28a909359a39c703a262814d0efd9b0bb5/test/captures/eap_request.packet -------------------------------------------------------------------------------- /test/captures/invalid_register.packet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retailnext/node-radius/5b55be28a909359a39c703a262814d0efd9b0bb5/test/captures/invalid_register.packet -------------------------------------------------------------------------------- /test/captures/motorola_accounting.packet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/retailnext/node-radius/5b55be28a909359a39c703a262814d0efd9b0bb5/test/captures/motorola_accounting.packet -------------------------------------------------------------------------------- /test/dictionaries/dictionary.airespace: -------------------------------------------------------------------------------- 1 | VENDOR Airespace 14179 2 | 3 | BEGIN-VENDOR Airespace 4 | ATTRIBUTE Airespace-Wlan-Id 1 integer 5 | ATTRIBUTE Airespace-QOS-Level 2 integer 6 | ATTRIBUTE Airespace-DSCP 3 integer 7 | ATTRIBUTE Airespace-8021p-Tag 4 integer 8 | ATTRIBUTE Airespace-Interface-Name 5 string 9 | ATTRIBUTE Airespace-ACL-Name 6 string 10 | 11 | VALUE Airespace-QOS-Level Bronze 3 12 | VALUE Airespace-QOS-Level Silver 0 13 | VALUE Airespace-QOS-Level Gold 1 14 | VALUE Airespace-QOS-Level Platinum 2 15 | 16 | END-VENDOR Airespace 17 | -------------------------------------------------------------------------------- /test/dictionaries/dictionary.aruba: -------------------------------------------------------------------------------- 1 | # Aruba attributes 2 | VENDORATTR 14823 Aruba-User-Role 1 string 3 | VENDORATTR 14823 Aruba-User-Vlan 2 integer 4 | VENDORATTR 14823 Aruba-Priv-Admin-User 3 integer 5 | VENDORATTR 14823 Aruba-Admin-Role 4 string 6 | VENDORATTR 14823 Aruba-Essid-Name 5 String 7 | VENDORATTR 14823 Aruba-Location-Id 6 string 8 | VENDORATTR 14823 Aruba-Port-Id 7 string 9 | VENDORATTR 14823 Aruba-Template-User 8 string 10 | VENDORATTR 14823 Aruba-Named-User-Vlan 9 string 11 | VENDORATTR 14823 Aruba-AP-Group 10 string 12 | -------------------------------------------------------------------------------- /test/dictionaries/dictionary.number_vendor_name: -------------------------------------------------------------------------------- 1 | VENDOR 123Foo 995486 2 | 3 | BEGIN-VENDOR 123Foo 4 | ATTRIBUTE 1Integer 1 integer 5 | END-VENDOR 123Foo 6 | 7 | VENDORATTR 995486 1String 2 string 8 | VENDORATTR 995486 12345 3 string 9 | -------------------------------------------------------------------------------- /test/dictionaries/dictionary.test1: -------------------------------------------------------------------------------- 1 | ATTRIBUTE Attribute-Test1 244 string 2 | 3 | $INCLUDE dictionary.test2 4 | -------------------------------------------------------------------------------- /test/dictionaries/dictionary.test2: -------------------------------------------------------------------------------- 1 | ATTRIBUTE Attribute-Test2 245 string 2 | 3 | # don't do anything crazy like loop forever 4 | $INCLUDE dictionary.test1 5 | -------------------------------------------------------------------------------- /test/dictionaries/dictionary.test_tunnel_type: -------------------------------------------------------------------------------- 1 | # Fake tunnel type to be loaded before ATTRIBUTE Tunnel-Type is defined 2 | 3 | VALUE Tunnel-Type TESTTUNNEL 31 4 | -------------------------------------------------------------------------------- /test/radius.test.js: -------------------------------------------------------------------------------- 1 | var testCase = require('nodeunit').testCase; 2 | var radius = require('../lib/radius'); 3 | var fs = require('fs'); 4 | var crypto = require('crypto'); 5 | 6 | var secret; 7 | 8 | var test_args = {}; 9 | 10 | module.exports = testCase({ 11 | setUp: function(callback) { 12 | secret = "nearbuy"; 13 | callback(); 14 | }, 15 | tearDown: function(callback) { 16 | radius.unload_dictionaries(); 17 | callback(); 18 | }, 19 | 20 | test_decode_mac_auth: function(test) { 21 | var raw_packet = fs.readFileSync(__dirname + '/captures/aruba_mac_auth.packet'); 22 | 23 | radius.load_dictionary(__dirname + '/dictionaries/dictionary.aruba'); 24 | 25 | var decoded = radius.decode({ packet: raw_packet, secret: secret }); 26 | 27 | test.equal( decoded.code, 'Access-Request' ); 28 | test.equal( decoded.identifier, 58 ); 29 | test.equal( decoded.length, 208 ); 30 | 31 | var expected_attrs = { 32 | 'NAS-IP-Address': '10.0.0.90', 33 | 'NAS-Port': 0, 34 | 'NAS-Port-Type': 'Wireless-802.11', 35 | 'User-Name': '7c:c5:37:ff:f8:af', 36 | 'User-Password': '7c:c5:37:ff:f8:af', 37 | 'Calling-Station-Id': '7CC537FFF8AF', 38 | 'Called-Station-Id': '000B86F02068', 39 | 'Service-Type': 'Login-User', 40 | 'Vendor-Specific': { 41 | 'Aruba-Essid-Name': 'muir-aruba-guest', 42 | 'Aruba-Location-Id': '00:1a:1e:c6:b0:ca', 43 | 'Aruba-AP-Group': 'cloud-cp' 44 | }, 45 | 'Message-Authenticator': new Buffer('f8a12329c7ed5a6e2568515243efb918', 'hex') 46 | }; 47 | test.deepEqual( decoded.attributes, expected_attrs ); 48 | 49 | test.done(); 50 | }, 51 | 52 | test_decode_mac_auth_without_secret: function(test) { 53 | var raw_packet = fs.readFileSync(__dirname + '/captures/aruba_mac_auth.packet'); 54 | 55 | radius.load_dictionary(__dirname + '/dictionaries/dictionary.aruba'); 56 | 57 | var decoded = radius.decode_without_secret({ packet: raw_packet }); 58 | 59 | test.equal( decoded.code, 'Access-Request' ); 60 | test.equal( decoded.identifier, 58 ); 61 | test.equal( decoded.length, 208 ); 62 | 63 | var expected_attrs = { 64 | 'NAS-IP-Address': '10.0.0.90', 65 | 'NAS-Port': 0, 66 | 'NAS-Port-Type': 'Wireless-802.11', 67 | 'User-Name': '7c:c5:37:ff:f8:af', 68 | 'User-Password': null, // this is an encrypted field, and so cannot be read without the password 69 | 'Calling-Station-Id': '7CC537FFF8AF', 70 | 'Called-Station-Id': '000B86F02068', 71 | 'Service-Type': 'Login-User', 72 | 'Vendor-Specific': { 73 | 'Aruba-Essid-Name': 'muir-aruba-guest', 74 | 'Aruba-Location-Id': '00:1a:1e:c6:b0:ca', 75 | 'Aruba-AP-Group': 'cloud-cp' 76 | }, 77 | 'Message-Authenticator': new Buffer('f8a12329c7ed5a6e2568515243efb918', 'hex') 78 | }; 79 | test.deepEqual( decoded.attributes, expected_attrs ); 80 | 81 | decoded = radius.decode({ 82 | secret: secret, 83 | packet: radius.encode({ 84 | secret: secret, 85 | code: "Access-Request", 86 | attributes: { 87 | 'User-Name': 'Caenogaean-asphyxia', 88 | 'User-Password': 'barratry-Wertherism' 89 | } 90 | }) 91 | }); 92 | 93 | test.equal( decoded.attributes['User-Password'], 'barratry-Wertherism' ); 94 | 95 | test.done(); 96 | }, 97 | 98 | // make sure everthing is fine with no dictionaries 99 | test_decode_no_dicts: function(test) { 100 | var raw_packet = fs.readFileSync(__dirname + '/captures/aruba_mac_auth.packet'); 101 | 102 | radius.unload_dictionaries(); 103 | var orig_load = radius.load_dictionary; 104 | radius.load_dictionary = function() { }; 105 | 106 | var decoded = radius.decode({ packet: raw_packet, secret: secret }); 107 | 108 | test.equal( decoded.code, 'Access-Request' ); 109 | test.equal( decoded.identifier, 58 ); 110 | test.equal( decoded.length, 208 ); 111 | 112 | // no pretty attributes 113 | test.deepEqual( decoded.attributes, {} ); 114 | 115 | var expected_raw_attrs = [ 116 | [4, new Buffer([10, 0, 0, 90])], 117 | [5, new Buffer([0, 0, 0, 0])], 118 | [61, new Buffer([0, 0, 0, 19])], 119 | [1, new Buffer('7c:c5:37:ff:f8:af')], 120 | [2, new Buffer('eb2ef7e83ec1a05e04fb5c6d91e088569a990fa2b1b2dc6a0f048596081164cd', 'hex')], 121 | [31, new Buffer('7CC537FFF8AF')], 122 | [30, new Buffer('000B86F02068')], 123 | [6, new Buffer([0, 0, 0, 1])], 124 | [26, new Buffer('000039e705126d7569722d61727562612d6775657374', 'hex')], 125 | [26, new Buffer('000039e7061330303a31613a31653a63363a62303a6361', 'hex')], 126 | [26, new Buffer('000039e70a0a636c6f75642d6370', 'hex')], 127 | [80, new Buffer('f8a12329c7ed5a6e2568515243efb918', 'hex')] 128 | ]; 129 | 130 | test.deepEqual( decoded.raw_attributes, expected_raw_attrs ); 131 | 132 | radius.load_dictionary = orig_load; 133 | 134 | test.done(); 135 | }, 136 | 137 | // can make a "naked" packet 138 | test_encode_access_request: function(test) { 139 | radius.load_dictionary(__dirname + '/dictionaries/dictionary.aruba'); 140 | 141 | var attributes = [ 142 | ['User-Name', 'ornithopter-aliptic'], 143 | ['User-Password', 'nucleohistone-overwilily'], 144 | ['Service-Type', 'Login-User'], 145 | ['NAS-IP-Address', '169.134.68.136'], 146 | 147 | ['Vendor-Specific', 14823, [ 148 | ['Aruba-User-Role', 'cracked-tylote'], 149 | [2, 825] 150 | ]], 151 | ['Vendor-Specific', 14823, [['Aruba-Essid-Name', 'phene-dentinalgia']]] 152 | ]; 153 | var packet = radius.encode({ 154 | code: 'Access-Request', 155 | identifier: 123, 156 | attributes: attributes, 157 | secret: secret 158 | }); 159 | 160 | var decoded = radius.decode({ packet: packet, secret: secret }); 161 | test.equal( decoded.code, 'Access-Request' ); 162 | test.equal( decoded.identifier, 123 ); 163 | 164 | var expected_attrs = { 165 | 'User-Name': 'ornithopter-aliptic', 166 | 'User-Password': 'nucleohistone-overwilily', 167 | 'Service-Type': 'Login-User', 168 | 'NAS-IP-Address': '169.134.68.136', 169 | 'Vendor-Specific': { 170 | 'Aruba-User-Role': 'cracked-tylote', 171 | 'Aruba-User-Vlan': 825, 172 | 'Aruba-Essid-Name': 'phene-dentinalgia' 173 | } 174 | }; 175 | test.deepEqual( decoded.attributes, expected_attrs ); 176 | 177 | test.done(); 178 | }, 179 | 180 | test_decode_hash_attributes: function(test) { 181 | var attrs = { 182 | 'User-Name': 'ornithopter-aliptic', 183 | 'User-Password': 'nucleohistone-overwilily', 184 | 'Service-Type': 'Login-User', 185 | 'NAS-IP-Address': '169.134.68.136' 186 | }; 187 | var packet = radius.encode({ 188 | code: 'Access-Request', 189 | identifier: 123, 190 | attributes: attrs, 191 | secret: secret 192 | }); 193 | 194 | var decoded = radius.decode({ packet: packet, secret: secret }); 195 | test.equal( decoded.code, 'Access-Request' ); 196 | test.equal( decoded.identifier, 123 ); 197 | test.deepEqual( decoded.attributes, attrs ); 198 | 199 | test.done(); 200 | }, 201 | 202 | test_throws_on_nested_hash_attributes: function(test) { 203 | var attrs = { 204 | 'User-Name': 'ornithopter-aliptic', 205 | 'User-Password': 'nucleohistone-overwilily', 206 | 'Service-Type': 'Login-User', 207 | 'NAS-IP-Address': '169.134.68.136', 208 | 'Vendor-Specific': { 209 | 'Aruba-User-Role': 'cracked-tylote', 210 | 'Aruba-User-Vlan': 825, 211 | 'Aruba-Essid-Name': 'phene-dentinalgia' 212 | } 213 | }; 214 | 215 | test.throws(function() { 216 | var packet = radius.encode({ 217 | code: 'Access-Request', 218 | identifier: 123, 219 | attributes: attrs, 220 | secret: secret 221 | }); 222 | }); 223 | test.done(); 224 | }, 225 | 226 | // test that our encoded packet matches bit-for-bit with a "real" 227 | // RADIUS packet 228 | test_encode_bit_for_bit: function(test) { 229 | var raw_packet = fs.readFileSync(__dirname + '/captures/aruba_mac_auth.packet'); 230 | 231 | radius.load_dictionary(__dirname + '/dictionaries/dictionary.aruba'); 232 | 233 | var encoded = radius.encode({ 234 | code: 'Access-Request', 235 | identifier: 58, 236 | authenticator: new Buffer('4a45fae086d9e114286b37b5f371ec6c', 'hex'), 237 | attributes: [ 238 | ['NAS-IP-Address', '10.0.0.90'], 239 | ['NAS-Port', 0], 240 | ['NAS-Port-Type', 'Wireless-802.11'], 241 | ['User-Name', '7c:c5:37:ff:f8:af'], 242 | ['User-Password', '7c:c5:37:ff:f8:af'], 243 | ['Calling-Station-Id', '7CC537FFF8AF'], 244 | ['Called-Station-Id', '000B86F02068'], 245 | ['Service-Type', 'Login-User'], 246 | ['Vendor-Specific', 14823, [['Aruba-Essid-Name', 'muir-aruba-guest']]], 247 | ['Vendor-Specific', 14823, [['Aruba-Location-Id', '00:1a:1e:c6:b0:ca']]], 248 | ['Vendor-Specific', 14823, [['Aruba-AP-Group', 'cloud-cp']]] 249 | ], 250 | secret: secret, 251 | add_message_authenticator: true 252 | }); 253 | 254 | test.equal( encoded.toString('hex'), raw_packet.toString('hex') ); 255 | 256 | test.done(); 257 | }, 258 | 259 | // encode will choose a random identifier for you if you don't provide one 260 | test_encode_random_identifer: function(test) { 261 | var decoded = radius.decode({ 262 | packet: radius.encode({ 263 | code: 'Access-Request', 264 | secret: secret 265 | }), 266 | secret: secret 267 | }); 268 | test.ok( decoded.identifier >= 0 && decoded.identifier < 256 ); 269 | 270 | var starting_id = decoded.identifier; 271 | 272 | // if you are unlucky this is an infinite loop 273 | while (true) { 274 | decoded = radius.decode({ 275 | packet: radius.encode({ 276 | code: 'Access-Request', 277 | secret: secret 278 | }), 279 | secret: secret 280 | }); 281 | if (decoded.identifier != starting_id) 282 | break; 283 | } 284 | 285 | test.ok( true ); 286 | 287 | test.done(); 288 | }, 289 | 290 | // given a previously decoded packet, prepare a response packet 291 | test_packet_response: function(test) { 292 | var raw_packet = fs.readFileSync(__dirname + '/captures/cisco_mac_auth.packet'); 293 | 294 | var decoded = radius.decode({ packet: raw_packet, secret: secret }); 295 | 296 | var response = radius.encode_response({ 297 | packet: decoded, 298 | code: 'Access-Reject', 299 | secret: secret 300 | }); 301 | 302 | var raw_response = fs.readFileSync(__dirname + '/captures/cisco_mac_auth_reject.packet'); 303 | test.equal( response.toString('hex'), raw_response.toString('hex') ); 304 | 305 | test.done(); 306 | }, 307 | 308 | // response needs to include proxy state 309 | test_response_include_proxy_state: function(test) { 310 | var request_with_proxy = radius.decode({ 311 | packet: radius.encode({ 312 | code: 'Access-Request', 313 | secret: secret, 314 | attributes: [ 315 | ['User-Name', 'ascribe-despairer'], 316 | ['Proxy-State', new Buffer('womanhouse-Pseudotsuga')], 317 | ['User-Password', 'ridiculous'], 318 | ['Proxy-State', new Buffer('regretfully-unstability')] 319 | ] 320 | }), 321 | secret: secret 322 | }); 323 | 324 | var decoded_response = radius.decode({ 325 | packet: radius.encode_response({ 326 | packet: request_with_proxy, 327 | code: 'Access-Reject', 328 | secret: secret 329 | }), 330 | secret: secret 331 | }); 332 | 333 | var expected_raw_attributes = [ 334 | [radius.attr_name_to_id('Proxy-State'), new Buffer('womanhouse-Pseudotsuga')], 335 | [radius.attr_name_to_id('Proxy-State'), new Buffer('regretfully-unstability')] 336 | ]; 337 | 338 | test.deepEqual( decoded_response.raw_attributes, expected_raw_attributes ); 339 | 340 | test.done(); 341 | }, 342 | 343 | // dont accidentally strip null bytes when encoding 344 | test_password_encode: function(test) { 345 | var decoded = radius.decode({ 346 | packet: radius.encode({ 347 | code: 'Access-Request', 348 | authenticator: new Buffer('426edca213c1bf6e005e90a64105ca3a', 'hex'), 349 | attributes: [['User-Password', 'ridiculous']], 350 | secret: secret 351 | }), 352 | secret: secret 353 | }); 354 | 355 | test.equal( decoded.attributes['User-Password'], 'ridiculous' ); 356 | 357 | test.done(); 358 | }, 359 | 360 | accounting_group: { 361 | setUp: function(cb) { 362 | radius.load_dictionary(__dirname + '/dictionaries/dictionary.airespace'); 363 | 364 | test_args = {}; 365 | test_args.raw_acct_request = fs.readFileSync(__dirname + '/captures/cisco_accounting.packet'); 366 | test_args.expected_acct_attrs = { 367 | 'User-Name': 'user_7C:C5:37:FF:F8:AF_134', 368 | 'NAS-Port': 1, 369 | 'NAS-IP-Address': '10.0.3.4', 370 | 'Framed-IP-Address': '10.2.0.252', 371 | 'NAS-Identifier': 'Cisco 4400 (Anchor)', 372 | 'Vendor-Specific': { 373 | 'Airespace-Wlan-Id': 2 374 | }, 375 | 'Acct-Session-Id': '4fecc41e/7c:c5:37:ff:f8:af/9', 376 | 'Acct-Authentic': 'RADIUS', 377 | 'Tunnel-Type': [0x00, 'VLAN'], 378 | 'Tunnel-Medium-Type': [0x00, 'IEEE-802'], 379 | 'Tunnel-Private-Group-Id': 5, 380 | 'Acct-Status-Type': 'Start', 381 | 'Calling-Station-Id': '7c:c5:37:ff:f8:af', 382 | 'Called-Station-Id': '00:22:55:90:39:60' 383 | }; 384 | cb(); 385 | }, 386 | 387 | test_accounting: function(test) { 388 | var raw_acct_request = test_args.raw_acct_request; 389 | var decoded = radius.decode({ packet: raw_acct_request, secret: secret }); 390 | 391 | var expected_attrs = test_args.expected_acct_attrs; 392 | 393 | test.deepEqual( decoded.attributes, expected_attrs ); 394 | 395 | // test we can encode the same packet 396 | var encoded = radius.encode({ 397 | code: 'Accounting-Request', 398 | identifier: decoded.identifier, 399 | secret: secret, 400 | attributes: [ 401 | ['User-Name', 'user_7C:C5:37:FF:F8:AF_134'], 402 | ['NAS-Port', 1], 403 | ['NAS-IP-Address', '10.0.3.4'], 404 | ['Framed-IP-Address', '10.2.0.252'], 405 | ['NAS-Identifier', 'Cisco 4400 (Anchor)'], 406 | ['Vendor-Specific', 'Airespace', [['Airespace-Wlan-Id', 2]]], 407 | ['Acct-Session-Id', '4fecc41e/7c:c5:37:ff:f8:af/9'], 408 | ['Acct-Authentic', 'RADIUS'], 409 | ['Tunnel-Type', 0x00, 'VLAN'], 410 | ['Tunnel-Medium-Type', 0x00, 'IEEE-802'], 411 | ['Tunnel-Private-Group-Id', '5'], 412 | ['Acct-Status-Type', 'Start'], 413 | ['Calling-Station-Id', '7c:c5:37:ff:f8:af'], 414 | ['Called-Station-Id', '00:22:55:90:39:60'] 415 | ] 416 | }); 417 | test.equal( encoded.toString('hex'), raw_acct_request.toString('hex') ); 418 | 419 | var raw_acct_response = fs.readFileSync(__dirname + 420 | '/captures/cisco_accounting_response.packet'); 421 | encoded = radius.encode_response({ 422 | packet: decoded, 423 | secret: secret, 424 | code: 'Accounting-Response' 425 | }); 426 | test.equal( encoded.toString('hex'), raw_acct_response.toString('hex') ); 427 | 428 | test.done(); 429 | }, 430 | 431 | test_invalid_accounting_packet_authenticator: function(test) { 432 | var raw_acct_request = test_args.raw_acct_request; 433 | var expected_attrs = test_args.expected_acct_attrs; 434 | 435 | // detect invalid accounting packets 436 | test.throws( function() { 437 | radius.decode({ packet: raw_acct_request, secret: 'not-secret' }); 438 | } ); 439 | 440 | try { 441 | radius.decode({ packet: raw_acct_request, secret: 'not-secret' }); 442 | } catch (err) { 443 | test.deepEqual( err.decoded.attributes, expected_attrs ); 444 | } 445 | test.done(); 446 | } 447 | }, 448 | 449 | test_no_empty_strings: function(test) { 450 | var decoded = radius.decode({ 451 | secret: secret, 452 | packet: radius.encode({ 453 | code: 'Access-Request', 454 | attributes: [['User-Name', '']], 455 | secret: secret 456 | }) 457 | }); 458 | 459 | // don't send empty strings (see RFC2865) 460 | test.deepEqual( decoded.attributes, {} ); 461 | 462 | test.done(); 463 | }, 464 | 465 | test_repeated_attribute: function(test) { 466 | var decoded = radius.decode({ 467 | secret: secret, 468 | packet: radius.encode({ 469 | secret: secret, 470 | code: 'Access-Reject', 471 | attributes: [ 472 | ['Reply-Message', 'message one'], 473 | ['Reply-Message', 'message two'] 474 | ] 475 | }) 476 | }); 477 | 478 | var expected_attrs = { 479 | 'Reply-Message': ['message one', 'message two'] 480 | }; 481 | test.deepEqual( decoded.attributes, expected_attrs ); 482 | 483 | test.done(); 484 | }, 485 | 486 | test_dictionary_include: function(test) { 487 | radius.unload_dictionaries(); 488 | radius.add_dictionary(__dirname + '/dictionaries/dictionary.test1'); 489 | 490 | var decoded = radius.decode({ 491 | secret: secret, 492 | packet: radius.encode({ 493 | secret: secret, 494 | code: 'Access-Request', 495 | attributes: [['Attribute-Test1', 'foo'], ['Attribute-Test2', 'bar']] 496 | }) 497 | }); 498 | 499 | var expected_attrs = { 500 | 'Attribute-Test1': 'foo', 501 | 'Attribute-Test2': 'bar' 502 | }; 503 | test.deepEqual( decoded.attributes, expected_attrs ); 504 | 505 | test.done(); 506 | }, 507 | 508 | // make sure we can load the dicts in any order 509 | test_dictionary_out_of_order: function(test) { 510 | var dicts = fs.readdirSync(__dirname + '/../dictionaries'); 511 | 512 | // make sure we can load any dictionary first 513 | for (var i = 0; i < dicts.length; i++) { 514 | radius.unload_dictionaries(); 515 | radius.load_dictionary(__dirname + '/../dictionaries/' + dicts[i]); 516 | } 517 | 518 | // and spot check things actually work loaded out of order 519 | radius.unload_dictionaries(); 520 | radius.load_dictionary(__dirname + '/../dictionaries/dictionary.rfc2867'); 521 | radius.load_dictionary(__dirname + '/../dictionaries/dictionary.rfc2866'); 522 | 523 | var decoded = radius.decode({ 524 | secret: secret, 525 | packet: radius.encode({ 526 | code: 'Accounting-Request', 527 | secret: secret, 528 | attributes: [ 529 | ['Acct-Status-Type', 'Tunnel-Reject'] 530 | ] 531 | }) 532 | }); 533 | 534 | test.equal( decoded.attributes['Acct-Status-Type'], 'Tunnel-Reject' ); 535 | 536 | radius.unload_dictionaries(); 537 | radius.load_dictionary(__dirname + '/dictionaries/dictionary.test_tunnel_type'); 538 | radius.load_dictionaries(); 539 | 540 | decoded = radius.decode({ 541 | secret: secret, 542 | packet: radius.encode({ 543 | code: 'Accounting-Request', 544 | secret: secret, 545 | attributes: [ 546 | ['Tunnel-Type', 0x00, 'TESTTUNNEL'] 547 | ] 548 | }) 549 | }); 550 | 551 | var expected_attrs = {'Tunnel-Type': [0x00, 'TESTTUNNEL']}; 552 | test.deepEqual( decoded.attributes, expected_attrs ); 553 | 554 | test.done(); 555 | }, 556 | 557 | test_zero_identifer: function(test) { 558 | var decoded = radius.decode({ 559 | packet: radius.encode({ 560 | secret: secret, 561 | code: 'Access-Request', 562 | identifier: 0 563 | }), 564 | secret: secret 565 | }); 566 | 567 | test.equal( decoded.identifier, 0 ); 568 | test.done(); 569 | }, 570 | 571 | test_date_type: function(test) { 572 | var raw_packet = fs.readFileSync(__dirname + '/captures/motorola_accounting.packet'); 573 | 574 | var decoded = radius.decode({ 575 | packet: raw_packet, 576 | secret: secret 577 | }); 578 | 579 | var epoch = 1349879753; 580 | 581 | test.equal( decoded.attributes['Event-Timestamp'].getTime(), epoch * 1000 ); 582 | 583 | var encoded = radius.encode({ 584 | code: 'Accounting-Request', 585 | identifier: decoded.identifier, 586 | attributes: [ 587 | ['User-Name', '00-1F-3B-8C-3A-15'], 588 | ['Acct-Status-Type', 'Start'], 589 | ['Acct-Session-Id', '1970D5A4-001F3B8C3A15-0000000001'], 590 | ['Calling-Station-Id', '00-1F-3B-8C-3A-15'], 591 | ['Called-Station-Id', 'B4-C7-99-77-59-D0:muir-moto-guest-site1'], 592 | ['NAS-Port', 1], 593 | ['NAS-Port-Type', 'Wireless-802.11'], 594 | ['NAS-IP-Address', '10.2.0.3'], 595 | ['NAS-Identifier', 'ap6532-70D5A4'], 596 | ['NAS-Port-Id', 'radio2'], 597 | ['Event-Timestamp', new Date(epoch * 1000)], 598 | ['Tunnel-Type', 0x00, 'VLAN' ], 599 | ['Tunnel-Medium-Type', 0x00, 'IEEE-802'], 600 | ['Tunnel-Private-Group-Id', '30'], 601 | ['Acct-Authentic', 'RADIUS'] 602 | ], 603 | secret: secret 604 | }); 605 | 606 | test.equal( encoded.toString('hex'), raw_packet.toString('hex') ); 607 | 608 | test.done(); 609 | }, 610 | 611 | test_date_type_non_mult_1000_ms: function(test) { 612 | var encoded; 613 | test.doesNotThrow(function() { 614 | encoded = radius.encode({ 615 | code: 'Accounting-Request', 616 | identifier: 123, 617 | attributes: [ 618 | ['Event-Timestamp', new Date(1403025894009)] 619 | ], 620 | secret: secret 621 | }); 622 | }); 623 | 624 | // truncates ms 625 | var decoded = radius.decode({ packet: encoded, secret: secret }); 626 | test.equal( decoded.attributes['Event-Timestamp'].getTime(), 1403025894000 ); 627 | 628 | test.done(); 629 | }, 630 | 631 | test_disconnect_request: function(test) { 632 | var encoded = radius.encode({ 633 | code: 'Disconnect-Request', 634 | identifier: 54, 635 | secret: secret, 636 | attributes: [ 637 | ['User-Name', 'mariticide-inquietation'], 638 | ['NAS-Identifier', 'Aglauros-charioted'] 639 | ] 640 | }); 641 | 642 | // check we did the non-user-password authenticator 643 | var got_authenticator = new Buffer(16); 644 | encoded.copy(got_authenticator, 0, 4); 645 | encoded.fill(0, 4, 20); 646 | 647 | var expected_authenticator = new Buffer(16); 648 | var hasher = crypto.createHash("md5"); 649 | hasher.update(encoded); 650 | hasher.update(secret); 651 | expected_authenticator.write(hasher.digest("binary"), 0, 16, "binary"); 652 | 653 | test.equal( got_authenticator.toString('hex'), expected_authenticator.toString('hex') ); 654 | 655 | // and make sure we check the authenticator when decoding 656 | test.throws(function() { 657 | radius.decode({ 658 | packet: encoded, 659 | secret: secret 660 | }); 661 | }); 662 | 663 | expected_authenticator.copy(encoded, 4, 0); 664 | test.doesNotThrow(function() { 665 | radius.decode({ 666 | packet: encoded, 667 | secret: secret 668 | }); 669 | }); 670 | 671 | test.done(); 672 | }, 673 | 674 | test_verify_response: function(test) { 675 | var request = radius.encode({ 676 | secret: secret, 677 | code: 'Accounting-Request', 678 | attributes: { 679 | 'User-Name': '00-1F-3B-8C-3A-15', 680 | 'Acct-Status-Type': 'Start' 681 | } 682 | }); 683 | 684 | var response = radius.encode_response({ 685 | secret: secret, 686 | code: 'Accounting-Response', 687 | packet: radius.decode({ packet: request, secret: secret }) 688 | }); 689 | 690 | test.ok( radius.verify_response({ 691 | request: request, 692 | response: response, 693 | secret: secret 694 | }) ); 695 | 696 | test.ok( !radius.verify_response({ 697 | request: request, 698 | response: response, 699 | secret: "Calliopsis-misbeholden" 700 | }) ); 701 | 702 | // response encoded with wrong secret 703 | response = radius.encode_response({ 704 | secret: "moyenne-paraboliform", 705 | code: 'Accounting-Response', 706 | packet: radius.decode({ packet: request, secret: secret }) 707 | }); 708 | test.ok( !radius.verify_response({ 709 | request: request, 710 | response: response, 711 | secret: secret 712 | }) ); 713 | 714 | test.done(); 715 | }, 716 | 717 | test_server_request: function(test) { 718 | var encoded1 = radius.encode({ 719 | code: 'Status-Server', 720 | identifier: 54, 721 | secret: secret, 722 | attributes: [ 723 | ['NAS-Identifier', 'symphilism-dicentrine'] 724 | ] 725 | }); 726 | 727 | var encoded2 = radius.encode({ 728 | code: 'Status-Server', 729 | identifier: 54, 730 | secret: secret, 731 | attributes: [ 732 | ['NAS-Identifier', 'symphilism-dicentrine'] 733 | ] 734 | }); 735 | 736 | // check we are doing a random authenticator 737 | var got_authenticator1 = new Buffer(16); 738 | encoded1.copy(got_authenticator1, 0, 4); 739 | 740 | var got_authenticator2 = new Buffer(16); 741 | encoded2.copy(got_authenticator2, 0, 4); 742 | 743 | test.notEqual( got_authenticator1.toString(), got_authenticator2.toString() ); 744 | 745 | var response = radius.encode_response({ 746 | code: 'Access-Accept', 747 | secret: secret, 748 | packet: radius.decode({packet: encoded1, secret: secret}) 749 | }); 750 | 751 | test.ok( radius.verify_response({ 752 | request: encoded1, 753 | response: response, 754 | secret: secret 755 | }) ); 756 | 757 | test.done(); 758 | }, 759 | 760 | test_vendor_names_with_numbers: function(test) { 761 | radius.load_dictionary(__dirname + '/dictionaries/dictionary.number_vendor_name'); 762 | 763 | var encoded = radius.encode({ 764 | code: "Access-Request", 765 | secret: secret, 766 | 767 | attributes: [ 768 | ['Vendor-Specific', '123Foo', [ 769 | ['1Integer', 478], 770 | ['1String', 'Zollernia-fibrovasal'], 771 | ['12345', 'myrmecophagoid-harn'] 772 | ]] 773 | ] 774 | }); 775 | 776 | var decoded = radius.decode({ 777 | packet: encoded, 778 | secret: secret 779 | }); 780 | 781 | test.equal( radius.vendor_name_to_id('123Foo'), 995486 ); 782 | 783 | test.deepEqual( decoded.attributes, { 784 | 'Vendor-Specific': { 785 | '1Integer': 478, 786 | '1String': 'Zollernia-fibrovasal', 787 | '12345': 'myrmecophagoid-harn' 788 | } 789 | } ); 790 | 791 | test.done(); 792 | }, 793 | 794 | message_authenticator_group: { 795 | setUp: function(cb) { 796 | secret = "testing123"; 797 | 798 | test_args = { 799 | raw_request: fs.readFileSync(__dirname + '/captures/eap_request.packet') 800 | }; 801 | test_args.parsed_request = radius.decode({ 802 | packet: test_args.raw_request, 803 | secret: secret 804 | }); 805 | cb(); 806 | }, 807 | 808 | // make sure we calculate the same Message-Authenticator 809 | test_calculate: function(test) { 810 | var attrs_without_ma = test_args.parsed_request.raw_attributes.filter(function(a) { 811 | return a[0] != radius.attr_name_to_id('Message-Authenticator'); 812 | }); 813 | 814 | var encoded = radius.encode({ 815 | code: test_args.parsed_request.code, 816 | identifier: test_args.parsed_request.identifier, 817 | authenticator: test_args.parsed_request.authenticator, 818 | attributes: attrs_without_ma, 819 | secret: secret 820 | }); 821 | 822 | test.equal( encoded.toString('hex'), test_args.raw_request.toString('hex') ); 823 | 824 | test.done(); 825 | }, 826 | 827 | // encode_response should calculate the appropriate Message-Authenticator 828 | test_encode_response: function(test) { 829 | var response = radius.encode_response({ 830 | code: "Access-Accept", 831 | secret: secret, 832 | packet: test_args.parsed_request 833 | }); 834 | 835 | var parsed_response = radius.decode({ 836 | packet: response, 837 | secret: secret 838 | }); 839 | 840 | // calculate expected Message-Authenticator 841 | 842 | var empty = new Buffer(16); 843 | empty.fill(0); 844 | 845 | var expected_response = radius.encode({ 846 | code: "Access-Accept", 847 | identifier: test_args.parsed_request.identifier, 848 | authenticator: test_args.parsed_request.authenticator, 849 | attributes: [["Message-Authenticator", empty]], 850 | secret: secret 851 | }); 852 | 853 | // expected_response's authenticator is correct, but Message-Authenticator is wrong 854 | // (it's all 0s). make sure verify_response checks both 855 | test.ok( !radius.verify_response({ 856 | request: test_args.raw_request, 857 | response: expected_response, 858 | secret: secret 859 | }) ); 860 | 861 | // put back the request's authenticator 862 | test_args.parsed_request.authenticator.copy(expected_response, 4); 863 | 864 | var expected_ma = radius.calculate_message_authenticator(expected_response, secret); 865 | test.equal( 866 | parsed_response.attributes["Message-Authenticator"].toString("hex"), 867 | expected_ma.toString("hex") 868 | ); 869 | 870 | test.ok( radius.verify_response({ 871 | request: test_args.raw_request, 872 | response: response, 873 | secret: secret 874 | }) ); 875 | 876 | test.done(); 877 | }, 878 | 879 | // response is missing Message-Authenticator, not okay 880 | test_response_missing_ma: function(test) { 881 | var bad_response = radius.encode({ 882 | code: "Access-Accept", 883 | identifier: test_args.parsed_request.identifier, 884 | authenticator: test_args.parsed_request.authenticator, 885 | attributes: [], 886 | secret: secret 887 | }); 888 | 889 | test.ok( !radius.verify_response({ 890 | request: test_args.raw_request, 891 | response: bad_response, 892 | secret: secret 893 | }) ); 894 | 895 | test.done(); 896 | }, 897 | 898 | // make sure we verify Message-Authenticator when decoding requests 899 | test_decode_verify: function(test) { 900 | test.throws(function() { 901 | radius.decode({ 902 | packet: test_args.raw_request, 903 | secret: 'wrong secret' 904 | }); 905 | }); 906 | 907 | test.done(); 908 | } 909 | }, 910 | 911 | test_utf8_strings: function(test) { 912 | var encoded = radius.encode({ 913 | secret: "密码", 914 | code: "Access-Request", 915 | attributes: { 916 | "User-Name": "金庸先生", 917 | "User-Password": "降龙十八掌" 918 | } 919 | }); 920 | 921 | var decoded = radius.decode({ 922 | packet: encoded, 923 | secret: "密码" 924 | }); 925 | 926 | test.deepEqual( { 927 | "User-Name": "金庸先生", 928 | "User-Password": "降龙十八掌" 929 | }, decoded.attributes ); 930 | 931 | test.done(); 932 | }, 933 | 934 | test_invalid_packet_attribute_length: function(test) { 935 | var invalid_packet = fs.readFileSync(__dirname + '/captures/invalid_register.packet'); 936 | var raw_packet = fs.readFileSync(__dirname + '/captures/aruba_mac_auth.packet'); 937 | 938 | // should fail decode packet attributes 939 | test.throws(function() { 940 | radius.decode_without_secret({ packet: invalid_packet }); 941 | } ); 942 | 943 | // should decode packet attributes 944 | test.doesNotThrow(function() { 945 | radius.decode_without_secret({ packet: raw_packet }); 946 | }); 947 | 948 | test.done(); 949 | }, 950 | 951 | test_tag_fields: function(test) { 952 | var decoded = radius.decode({ 953 | secret: secret, 954 | packet: radius.encode({ 955 | code: 'Accounting-Request', 956 | secret: secret, 957 | attributes: [ 958 | ['Tunnel-Type', 0x01, 'VLAN'], 959 | ['User-Name', 'honeymooner-hitched'], 960 | ] 961 | }) 962 | }); 963 | 964 | test.deepEqual( { 965 | 'Tunnel-Type': [ 1, 'VLAN'], 966 | 'User-Name': 'honeymooner-hitched' 967 | }, decoded.attributes ); 968 | test.done(); 969 | } 970 | }); 971 | --------------------------------------------------------------------------------