├── tests
├── bootstrap.php
└── ClientTest.php
├── lib
├── Crypt
│ ├── CHAP.php
│ └── CHAP
│ │ ├── MD5.php
│ │ ├── MSv1.php
│ │ └── MSv2.php
└── Pear_CHAP.php
├── phpunit.xml
├── .travis.yml
├── autoload.php
├── composer.json
├── example
├── eapmschapv2.php
├── client.php
└── mschap.php
├── src
├── MsChapV2Packet.php
├── EAPPacket.php
├── VendorId.php
└── Radius.php
├── LICENSE
└── README.md
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 | tests
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 |
3 | language: php
4 |
5 | matrix:
6 | include:
7 | - php: 7.3
8 | dist: bionic
9 | - php: 7.4
10 | dist: bionic
11 | - php: 8.0
12 | dist: bionic
13 | - php: 8.1.0
14 | dist: bionic
15 |
16 | before_install:
17 | - composer require --dev --no-update phpunit/phpunit ^9.5
18 |
19 | before_script: composer install
20 |
21 | script:
22 | - vendor/bin/phpunit
23 |
24 |
--------------------------------------------------------------------------------
/autoload.php:
--------------------------------------------------------------------------------
1 | 2) {
16 | if ($parts[0] == 'Dapphp' && $parts[1] == 'Radius') {
17 | require_once __DIR__ . '/src/' . $parts[2] . '.php';
18 | }
19 | }
20 | });
21 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dapphp/radius",
3 | "description": "A pure PHP RADIUS client based on the SysCo/al implementation",
4 | "type": "library",
5 | "keywords": ["radius","authentication","authorization","pap","chap","ms-chap","ms-chap v2","rfc2865","rfc1994","rfc2284","rfc2869","rfc2759"],
6 | "homepage": "https://github.com/dapphp/radius",
7 | "require": {
8 | "php": "^7.3 || ^8.0"
9 | },
10 | "require-dev": {
11 | "phpunit/phpunit": "^9.5.13"
12 | },
13 | "suggest": {
14 | "ext-openssl": "To support hashing required by Pear_CHAP"
15 | },
16 | "license": "LGPL-3.0-or-later",
17 | "authors": [
18 | {
19 | "name": "Drew Phillips",
20 | "email": "drew@drew-phillips.com",
21 | "homepage": "https://drew-phillips.com/"
22 | },
23 | {
24 | "name": "SysCo/al",
25 | "homepage": "http://developer.sysco.ch/php/"
26 | }
27 | ],
28 | "autoload": {
29 | "psr-0": {
30 | "Crypt_CHAP_": "lib/"
31 | },
32 | "psr-4": {
33 | "Dapphp\\Radius\\": "src/"
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/example/eapmschapv2.php:
--------------------------------------------------------------------------------
1 | setServer($server) // IP or hostname of RADIUS server
22 | ->setSecret($secret) // RADIUS shared secret
23 | ->setAttribute(32, 'login') // NAS port
24 | ->setDebug((bool)$debug);
25 |
26 | // Send access request for user nemo
27 | echo "Sending EAP-MSCHAP-v2 access request to $server with username $user\n";
28 | $response = $radius->accessRequestEapMsChapV2($user, $pass);
29 |
30 | if ($response === false) {
31 | // false returned on failure
32 | echo sprintf("Access-Request failed with error %d (%s).\n",
33 | $radius->getErrorCode(),
34 | $radius->getErrorMessage()
35 | );
36 | } else {
37 | // access request was accepted - client authenticated successfully
38 | echo "Success! Received Access-Accept response from RADIUS server.\n";
39 | }
40 |
--------------------------------------------------------------------------------
/example/client.php:
--------------------------------------------------------------------------------
1 | setServer($server) // IP or hostname of RADIUS server
20 | ->setSecret($secret) // RADIUS shared secret
21 | ->setNasIpAddress('127.0.0.1') // IP or hostname of NAS (device authenticating user)
22 | ->setAttribute(32, 'vpn') // NAS identifier
23 | ->setDebug((bool)$debug); // Enable debug output to screen/console
24 |
25 | // Send access request for a user with username = 'username' and password = 'password!'
26 | echo "Sending access request to $server with username $user\n";
27 | $response = $radius->accessRequest($user, $pass);
28 |
29 | if ($response === false) {
30 | // false returned on failure
31 | echo sprintf("Access-Request failed with error %d (%s).\n",
32 | $radius->getErrorCode(),
33 | $radius->getErrorMessage()
34 | );
35 | } else {
36 | // access request was accepted - client authenticated successfully
37 | echo "Success! Received Access-Accept response from RADIUS server.\n";
38 | }
39 |
--------------------------------------------------------------------------------
/example/mschap.php:
--------------------------------------------------------------------------------
1 | setServer($server) // IP or hostname of RADIUS server
22 | ->setSecret($secret) // RADIUS shared secret
23 | ->setNasIpAddress('127.0.0.1') // IP or hostname of NAS (device authenticating user)
24 | ->setNasPort(20) // NAS port
25 | ->setDebug((bool)$debug);
26 |
27 | $radius->setMSChapPassword($pass); // set mschapv1 password for user
28 |
29 | // Send access request for user nemo
30 | echo "Sending MS-CHAP access request to $server with username $user\n";
31 | $response = $radius->accessRequest($user);
32 |
33 | if ($response === false) {
34 | // false returned on failure
35 | echo sprintf("Access-Request failed with error %d (%s).\n",
36 | $radius->getErrorCode(),
37 | $radius->getErrorMessage()
38 | );
39 | } else {
40 | // access request was accepted - client authenticated successfully
41 | echo "Success! Received Access-Accept response from RADIUS server.\n";
42 | }
43 |
--------------------------------------------------------------------------------
/src/MsChapV2Packet.php:
--------------------------------------------------------------------------------
1 | opcode = ord($packet[0]);
42 | $p->msChapId = ord($packet[1]);
43 | $temp = unpack('n', substr($packet, 2, 2));
44 | $p->msLength = array_shift($temp);
45 | $p->valueSize = ord($packet[4]);
46 |
47 | switch($p->opcode) {
48 | case self::OPCODE_CHALLENGE: // challenge
49 | $p->challenge = substr($packet, 5, 16);
50 | $p->name = substr($packet, -($p->msLength + 5 - $p->valueSize - 10));
51 | break;
52 |
53 | case self::OPCODE_RESPONSE: // response
54 | break;
55 |
56 | case self::OPCODE_SUCCESS: // success
57 | break;
58 |
59 | case self::OPCODE_FAILURE: // failure
60 | $p->response = substr($packet, 4);
61 | break;
62 | }
63 |
64 | return $p;
65 | }
66 |
67 | /**
68 | * Convert a packet structure to a byte string for sending over the wire
69 | * @return string MS-CHAP-V2 packet string
70 | */
71 | public function __toString()
72 | {
73 | $packet = pack('C', $this->opcode) .
74 | chr($this->msChapId) .
75 | "\x00\x00"; // temp length
76 |
77 | switch($this->opcode) {
78 | case self::OPCODE_CHALLENGE: // challenge
79 | $packet .= chr(16);
80 | $packet .= $this->challenge;
81 | $packet .= $this->name;
82 | break;
83 |
84 | case self::OPCODE_RESPONSE: // response
85 | $packet .= chr(49);
86 | $packet .= $this->challenge;
87 | $packet .= str_repeat("\x00", 8); // reserved
88 | $packet .= $this->response;
89 | $packet .= chr(0); // reserved flags
90 | $packet .= $this->name;
91 | break;
92 |
93 | case self::OPCODE_SUCCESS: // success
94 | return chr(3);
95 |
96 | case self::OPCODE_FAILURE: // failure
97 | return chr(4);
98 |
99 | case self::OPCODE_CHANGEPASS: // changepass [RFC2759]
100 | $packet .= $this->encryptedPwd; // 516 Section 8.9
101 | $packet .= $this->encryptedHash; // 16 Section 8.12
102 | $packet .= $this->challenge; // 16 Response packet description
103 | $packet .= str_repeat("\x00", 8); // 8 reserved
104 | $packet .= $this->response; // 24 ntresponse
105 | $packet .= "\x00\x00"; // 2 flags, always 0
106 | break;
107 | }
108 |
109 | $length = pack('n', strlen($packet));
110 | $packet[2] = $length[0];
111 | $packet[3] = $length[1];
112 |
113 | return $packet;
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/EAPPacket.php:
--------------------------------------------------------------------------------
1 | setId($id);
41 | $packet->code = self::CODE_RESPONSE;
42 | $packet->type = self::TYPE_IDENTITY;
43 | $packet->data = $identity;
44 |
45 | return $packet->__toString();
46 | }
47 |
48 | /**
49 | * Helper function to generate an EAP Legacy NAK packet
50 | *
51 | * @param string $desiredAuth The desired auth method
52 | * @param int $id The packet ID, given by server at predecessing proposal
53 | * @return string An EAP Legacy NAK packet
54 | */
55 | public static function legacyNak($desiredAuth, $id)
56 | {
57 | $packet = new self();
58 | $packet->setId($id);
59 | $packet->code = self::CODE_RESPONSE;
60 | $packet->type = self::TYPE_NAK;
61 | $packet->data = chr($desiredAuth);
62 |
63 | return $packet->__toString();
64 | }
65 |
66 | /**
67 | * Helper function to generate an EAP Success packet
68 | *
69 | * @param string $desiredAuth The identity (username) to send in the packet
70 | * @param int $id The packet ID, given by server at predecessing proposal
71 | * @return string An EAP Legacy NAK packet
72 | */
73 | public static function eapSuccess($id)
74 | {
75 | $eapSuccess = new MsChapV2Packet();
76 | $eapSuccess->opcode = MsChapV2Packet::OPCODE_SUCCESS;
77 |
78 | $packet = self::mschapv2($eapSuccess, $id);
79 |
80 | return $packet;
81 | }
82 |
83 | /**
84 | * Helper function for sending an MS-CHAP-V2 packet encapsulated in an EAP packet
85 | *
86 | * @param \Dapphp\Radius\MsChapV2Packet $chapPacket The MSCHAP v2 packet to send
87 | * @param int $id The CHAP packet identifier (random if omitted)
88 | * @return string An EAP packet with embedded MS-CHAP-V2 packet in the data field
89 | */
90 | public static function mschapv2(\Dapphp\Radius\MsChapV2Packet $chapPacket, $id = null)
91 | {
92 | $packet = new self();
93 | $packet->setId($id);
94 | $packet->code = self::CODE_RESPONSE;
95 | $packet->type = self::TYPE_EAP_MS_AUTH;
96 | $packet->data = $chapPacket->__toString();
97 |
98 | return $packet->__toString();
99 | }
100 |
101 | /**
102 | * Convert a raw EAP packet into a structure
103 | *
104 | * @param string $packet The EAP packet
105 | * @return \Dapphp\Radius\EAPPacket The parsed packet structure
106 | */
107 | public static function fromString($packet)
108 | {
109 | // TODO: validate incoming packet better
110 |
111 | $p = new self();
112 | $p->code = ord($packet[0]);
113 | $p->id = ord($packet[1]);
114 | $temp = unpack('n', substr($packet, 2, 2));
115 | $length = array_shift($temp);
116 |
117 | if (strlen($packet) != $length) {
118 | return false;
119 | }
120 |
121 | $p->type = ord(substr($packet, 4, 1));
122 | $p->data = substr($packet, 5);
123 |
124 | return $p;
125 | }
126 |
127 | /**
128 | * Set the ID of the EAP packet
129 | * @param int $id The EAP packet ID
130 | * @return \Dapphp\Radius\EAPPacket Fluent interface
131 | */
132 | public function setId($id = null)
133 | {
134 | if (is_null($id)) {
135 | $this->id = mt_rand(0, 255);
136 | } else {
137 | $this->id = (int)$id;
138 | }
139 |
140 | return $this;
141 | }
142 |
143 | /**
144 | * Convert the packet to a raw byte string
145 | *
146 | * @return string The packet as a byte string for sending over the wire
147 | */
148 | public function __toString()
149 | {
150 | return chr($this->code) .
151 | chr($this->id) .
152 | pack('n', 5 + strlen($this->data)) .
153 | chr($this->type) .
154 | $this->data;
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/src/VendorId.php:
--------------------------------------------------------------------------------
1 |
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ## Name:
8 |
9 | **Dapphp\Radius** - A pure PHP RADIUS client based on the SysCo/al implementation
10 |
11 | ## Author:
12 |
13 | * Drew Phillips
14 | * SysCo/al (http://developer.sysco.ch/php/)
15 |
16 | ## Description:
17 |
18 | **Dapphp\Radius** is a pure PHP RADIUS client for authenticating users against
19 | a RADIUS server in PHP. It currently supports basic RADIUS auth using PAP,
20 | CHAP (MD5), MSCHAP v1, and EAP-MSCHAP v2. The current 2.5.x branch is tested
21 | to work with the following RADIUS servers:
22 |
23 | - Microsoft Windows Server 2019 Network Policy Server
24 | - Microsoft Windows Server 2016 Network Policy Server
25 | - Microsoft Windows Server 2012 Network Policy Server
26 | - FreeRADIUS 2 and above
27 |
28 | PAP authentication has been tested on:
29 |
30 | - Microsoft Radius server IAS
31 | - Mideye RADIUS Server
32 | - Radl
33 | - RSA SecurID
34 | - VASCO Middleware 3.0 server
35 | - WinRadius
36 | - ZyXEL ZyWALL OTP
37 |
38 | The PHP openssl extension is required if using MSCHAP v1 or v2. For older PHP
39 | versions that have mcrypt without openssl support, then mcrypt is used.
40 |
41 | ## Installation:
42 |
43 | The recommended way to install `dapphp/radius` is using [Composer](https://getcomposer.org).
44 | If you are already using composer, simple run `composer require dapphp/radius` or add
45 | `dapphp/radius` to your composer.json file's `require` section.
46 |
47 | Standalone installation is also supported and a SPL autoloader is provided.
48 | (Don't use the standalone autoloader if you're using Composer!).
49 |
50 | To install standalone, download the release archive and extract to a location
51 | on your server. In your application, `require_once 'radius/autoload.php';` and
52 | then you can use the class.
53 |
54 | ## Examples:
55 |
56 | See the `examples/` directory for working examples. The RADIUS server address, secret, and credentials are read from
57 | environment variables and default to:
58 |
59 | RADIUS_SERVER_ADDR=192.168.0.20
60 | RADIUS_USER=nemo
61 | RADIUS_PASS=arctangent
62 | RADIUS_SECRET=xyzzy5461
63 |
64 | To print RADIUS debug info, specify the `-v` option.
65 |
66 | Example:
67 |
68 | RADIUS_SERVER_ADDR=10.0.100.1 RADIUS_USER=radtest php example/client.php -v
69 |
70 | ## Synopsis:
71 |
72 | setServer('12.34.56.78') // RADIUS server address
84 | ->setSecret('radius shared secret')
85 | ->setNasIpAddress('10.0.1.2') // NAS server address
86 | ->setAttribute(32, 'login'); // NAS identifier
87 |
88 | // PAP authentication; returns true if successful, false otherwise
89 | $authenticated = $client->accessRequest($username, $password);
90 |
91 | // CHAP-MD5 authentication
92 | $client->setChapPassword($password); // set chap password
93 | $authenticated = $client->accessRequest($username); // authenticate, don't specify pw here
94 |
95 | // MSCHAP v1 authentication
96 | $client->setMSChapPassword($password); // set ms chap password (uses openssl or mcrypt)
97 | $authenticated = $client->accessRequest($username);
98 |
99 | // EAP-MSCHAP v2 authentication
100 | $authenticated = $client->accessRequestEapMsChapV2($username, $password);
101 |
102 | if ($authenticated === false) {
103 | // false returned on failure
104 | echo sprintf(
105 | "Access-Request failed with error %d (%s).\n",
106 | $client->getErrorCode(),
107 | $client->getErrorMessage()
108 | );
109 | } else {
110 | // access request was accepted - client authenticated successfully
111 | echo "Success! Received Access-Accept response from RADIUS server.\n";
112 | }
113 |
114 | ## Advanced Usage:
115 |
116 | // Authenticating against a RADIUS cluster (each server needs the same secret).
117 | // Each server in the list is tried until auth success or failure. The
118 | // next server is tried on timeout or other error.
119 | // Set the secret and any required attributes first.
120 |
121 | $servers = [ 'server1.radius.domain', 'server2.radius.domain' ];
122 | // or
123 | $servers = gethostbynamel("radius.site.domain"); // gets list of IPv4 addresses to a given host
124 |
125 | $authenticated = $client->accessRequestList($servers, $username, $password);
126 | // or
127 | $authenticated = $client->accessRequestEapMsChapV2List($servers, $username, $password);
128 |
129 |
130 | // Setting vendor specific attributes
131 | // Many vendor IDs are available in \Dapphp\Radius\VendorId
132 | // e.g. \Dapphp\Radius\VendorId::MICROSOFT
133 | $client->setVendorSpecificAttribute($vendorId, $attributeNumber, $rawValue);
134 |
135 | // Retrieving attributes from RADIUS responses after receiving a failure or success response
136 | $value = $client->getAttribute($attributeId);
137 |
138 | // Get an array of all received attributes
139 | $attributes = getReceivedAttributes();
140 |
141 | // Debugging
142 | // Prior to sending a request, call
143 | $client->setDebug(true); // enable debug output on console
144 | // Shows what attributes are sent and received, and info about the request/response
145 |
146 |
147 | ## Requirements:
148 |
149 | * PHP 5.3 or greater
150 |
151 | ## TODO:
152 |
153 | - Set attributes by name, rather than number
154 | - Vendor specific attribute dictionaries?
155 | - Test with more implementations and confirm working
156 | - Accounting?
157 |
158 | ## Copyright:
159 |
160 | Copyright (c) 2008, SysCo systemes de communication sa
161 | SysCo (tm) is a trademark of SysCo systemes de communication sa
162 | (http://www.sysco.ch/)
163 | All rights reserved.
164 |
165 | Copyright (c) 2018, Drew Phillips
166 | (https://drew-phillips.com)
167 |
168 | Pure PHP radius class is free software; you can redistribute it and/or
169 | modify it under the terms of the GNU Lesser General Public License as
170 | published by the Free Software Foundation, either version 3 of the License,
171 | or (at your option) any later version.
172 |
173 | Pure PHP radius class is distributed in the hope that it will be useful,
174 | but WITHOUT ANY WARRANTY; without even the implied warranty of
175 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
176 | GNU Lesser General Public License for more details.
177 |
178 | You should have received a copy of the GNU Lesser General Public
179 | License along with Pure PHP radius class.
180 | If not, see
181 |
182 | ## Licenses:
183 |
184 | This library makes use of the Crypt_CHAP PEAR library. See `lib/Pear_CHAP.php`.
185 |
186 | Copyright (c) 2002-2010, Michael Bretterklieber
187 | All rights reserved.
188 |
189 | Redistribution and use in source and binary forms, with or without
190 | modification, are permitted provided that the following conditions
191 | are met:
192 |
193 | 1. Redistributions of source code must retain the above copyright
194 | notice, this list of conditions and the following disclaimer.
195 | 2. Redistributions in binary form must reproduce the above copyright
196 | notice, this list of conditions and the following disclaimer in the
197 | documentation and/or other materials provided with the distribution.
198 | 3. The names of the authors may not be used to endorse or promote products
199 | derived from this software without specific prior written permission.
200 |
201 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
202 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
203 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
204 | IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
205 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
206 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
207 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
208 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
209 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
210 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
211 |
212 | This code cannot simply be copied and put under the GNU Public License or
213 | any other GPL-like (LGPL, GPL2) License.
214 |
--------------------------------------------------------------------------------
/tests/ClientTest.php:
--------------------------------------------------------------------------------
1 | setAttribute(80, $test);
20 | $attr = $client->getAttributesToSend(80);
21 | $this->assertEquals($test, $attr);
22 |
23 | $client->removeAttribute(80);
24 | $attr = $client->getAttributesToSend(80);
25 | $this->assertEquals(null, $attr);
26 |
27 | // integer value test
28 | $nasPort = 32768;
29 |
30 | $client->setAttribute(5, $nasPort);
31 | $attr = $client->getAttributesToSend(5);
32 | $this->assertEquals($nasPort, $attr);
33 |
34 | $client->removeAttribute(5);
35 | $attr = $client->getAttributesToSend(5);
36 | $this->assertEquals(null, $attr);
37 | }
38 |
39 | public function testGetAttributes()
40 | {
41 | $client = new Radius();
42 | $username = 'LinusX2@arpa.net';
43 | $nasIp = '192.168.88.1';
44 | $nasPort = 64000;
45 |
46 | $expected = ''; // manually constructed hex string
47 | $expected .= chr(1); // username
48 | $expected .= chr(2 + strlen($username)); // length
49 | $expected .= $username;
50 |
51 | $expected .= chr(4); // nas ip
52 | $expected .= chr(6);
53 | $expected .= pack('N', ip2long($nasIp));
54 |
55 | $expected .= chr(5); // nas port
56 | $expected .= chr(6);
57 | $expected .= pack('N', $nasPort);
58 |
59 |
60 | $client->setUsername($username)
61 | ->setNasIPAddress($nasIp)
62 | ->setNasPort($nasPort);
63 |
64 | $actual = implode('', $client->getAttributesToSend());
65 |
66 | $this->assertEquals($expected, $actual);
67 | $this->assertEquals($username, $client->getAttributesToSend(1));
68 | $this->assertEquals($nasIp, $client->getAttributesToSend(4));
69 | $this->assertEquals($nasPort, $client->getAttributesToSend(5));
70 | }
71 |
72 | public function testEncryptedPassword()
73 | {
74 | $pass = 'arctangent';
75 | $secret = 'xyzzy5461';
76 | $requestAuthenticator = "\x0f\x40\x3f\x94\x73\x97\x80\x57\xbd\x83\xd5\xcb\x98\xf4\x22\x7a";
77 | $client = new Radius();
78 |
79 | $expected = "\x0d\xbe\x70\x8d\x93\xd4\x13\xce\x31\x96\xe4\x3f\x78\x2a\x0a\xee";
80 | $encrypted = $client->getEncryptedPassword($pass, $secret, $requestAuthenticator);
81 |
82 | $this->assertEquals($expected, $encrypted);
83 | }
84 |
85 | public function testEncryptedPassword2()
86 | {
87 | $pass = 'm1cr0$ofT_W1nDoWz*';
88 | $secret = '%iM8WD3(9bSh4jXNyOH%4W6RE1s4bfQ#0h*n^lOz';
89 | $requestAuthenticator = "\x7d\x22\x56\x6c\x9d\x2d\x50\x26\x88\xc5\xb3\xf9\x33\x77\x14\x55";
90 | $client = new Radius();
91 |
92 | $expected = "\x44\xe0\xac\xdc\xed\x56\x39\x67\xb1\x41\x90\xef\x3e\x10\xca\x2c\xb5\xb0\x5f\xf6\x6c\x31\x87\xf0\x2a\x92\xcb\x65\xeb\x97\x31\x1f";
93 | $encrypted = $client->getEncryptedPassword($pass, $secret, $requestAuthenticator);
94 |
95 | $this->assertEquals($expected, $encrypted);
96 | }
97 |
98 | public function testCryptCHAPMSv1()
99 | {
100 | $pass = "Don't forget to bring a passphrase!";
101 |
102 | $chap = new \Crypt_CHAP_MSv1();
103 | $chap->password = $pass;
104 | $chap->chapid = 42;
105 | $chap->challenge = "\x6c\x7e\x0d\xba\xe3\x81\xea\x51";
106 |
107 | $response = $chap->ntChallengeResponse();
108 |
109 | $this->assertEquals('5f169b7d8176516f8092bce99008e097febfed2f043ec04e', bin2hex($response));
110 | }
111 |
112 | public function testCryptCHAPMSv1Indirect()
113 | {
114 | // Ensure Pear_CHAP_MSv1 can be loaded by Radius and that setting the attributes works
115 |
116 | $pass = "This is ms-chap ~~++";
117 | $chal = "\x02\x04\x08\x10\x20\x40\x80\x00"; // 8 byte 'random' challenge
118 | $client = new Radius();
119 |
120 | $client->setMsChapPassword($pass, $chal);
121 |
122 | $chapChallenge = $client->getAttributesToSend(26);
123 |
124 | $vendor = unpack('NID', substr($chapChallenge, 0, 4));
125 | $type = ord(substr($chapChallenge, 4, 1));
126 | $length = ord(substr($chapChallenge, 5, 1));
127 | $data = substr($chapChallenge, 6, $length);
128 |
129 | $this->assertEquals(VendorId::MICROSOFT, $vendor['ID']);
130 | $this->assertEquals(11, $type); // chap challenge
131 | $this->assertEquals($chal, $data);
132 | }
133 |
134 | public function testCryptCHAPMSv2()
135 | {
136 | $pass = 'Passwords < Passphrases < $whatsNext?';
137 |
138 | $chap = new \Crypt_CHAP_MSv2();
139 | $chap->username = 'nemo';
140 | $chap->password = $pass;
141 | $chap->chapid = 37;
142 | $chap->authChallenge = "\x01\x23\x45\x67\x89\xAB\xCD\xEF\xFE\xDC\xBA\x98\x76\x54\x32\x10";
143 | $chap->peerChallenge = "\x93\xa8\x14\xc3\x90\x4e\x67\xcc\xb1\xd2\x72\x23\xd5\xf3\x90\xae";
144 |
145 | $response = $chap->challengeResponse();
146 |
147 | $this->assertEquals('a3d12ce2f52d13fe04421205a2ce17b0e559ea8a9e594c1c', bin2hex($response));
148 | }
149 |
150 | public function testAuthenticationPacket()
151 | {
152 | $user = 'nemo';
153 | $pass = 'arctangent';
154 | $secret = 'xyzzy5461';
155 | $nas = '192.168.1.16';
156 | $nasPort = 3;
157 |
158 | $client = new Radius();
159 |
160 | $client->setRequestAuthenticator("\x0f\x40\x3f\x94\x73\x97\x80\x57\xbd\x83\xd5\xcb\x98\xf4\x22\x7a");
161 |
162 | $client->setPacketType(Radius::TYPE_ACCESS_REQUEST)
163 | ->setSecret($secret)
164 | ->setUsername($user)
165 | ->setPassword($pass)
166 | ->setNasIPAddress($nas)
167 | ->setNasPort($nasPort);
168 |
169 | $packet = $client->generateRadiusPacket();
170 | $pwEnc = "\x0d\xbe\x70\x8d\x93\xd4\x13\xce\x31\x96\xe4\x3f\x78\x2a\x0a\xee";
171 | $expected = "\x01\x00\x00\x38\x0f\x40\x3f\x94\x73\x97\x80\x57\xbd\x83"
172 | . "\xd5\xcb\x98\xf4\x22\x7a\x01\x06\x6e\x65\x6d\x6f\x02\x12"
173 | . $pwEnc
174 | . "\x04\x06\xc0\xa8\x01\x10\x05\x06\x00\x00\x00\x03";
175 |
176 | $this->assertEquals($expected, $packet);
177 | }
178 |
179 | public function testFramedAuthPacket()
180 | {
181 | $user = 'flopsy';
182 | $pass = 'arctangent';
183 | $reqAuth = "\x2a\xee\x86\xf0\x8d\x0d\x55\x96\x9c\xa5\x97\x8e\x0d\x33\x67\xa2";
184 | $nas = '192.168.1.16';
185 | $nasPort = 20;
186 |
187 | $expected = "\x01\x01\x00\x47\x2a\xee\x86\xf0\x8d\x0d\x55\x96\x9c\xa5"
188 | ."\x97\x8e\x0d\x33\x67\xa2\x01\x08\x66\x6c\x6f\x70\x73\x79"
189 | ."\x03\x13\x16\xe9\x75\x57\xc3\x16\x18\x58\x95\xf2\x93\xff"
190 | ."\x63\x44\x07\x72\x75\x04\x06\xc0\xa8\x01\x10\x05\x06\x00"
191 | ."\x00\x00\x14\x06\x06\x00\x00\x00\x02\x07\x06\x00\x00\x00\x01";
192 |
193 | $client = new Radius();
194 | $client->getNextIdentifier(); // increment to 1 for test
195 | $client->setChapId(22);
196 | $client->setRequestAuthenticator($reqAuth)
197 | ->setPacketType(Radius::TYPE_ACCESS_REQUEST)
198 | ->setUsername($user)
199 | ->setChapPassword($pass)
200 | ->setNasIPAddress($nas)
201 | ->setNasPort($nasPort)
202 | ->setAttribute(6, 2) // service type (6) = framed (2)
203 | ->setAttribute(7, 1); // framed protocol (7) = ppp (1)
204 |
205 | $packet = $client->generateRadiusPacket();
206 |
207 | $this->assertEquals($expected, $packet);
208 | }
209 |
210 | public function testHmacMd5()
211 | {
212 | $str = hex2bin('01870082093e4ad125399f8ac4ba6b00ab69a04001066e656d6f04067f0000010506000000145012000000000000000000000000000000001a10000001370b0a740c7921e45e91391a3a00000137013400010000000000000000000000000000000000000000000000004521bd46aebfd2ab3ec21dd6e6bbfa2e4ff325eab720fe37');
213 | $hash = hash_hmac('md5', $str, 'xyzzy5461', true);
214 |
215 | $expected = '48a3704ac91e8191497a1f3f213eb338';
216 | $actual = bin2hex($hash);
217 |
218 | $this->assertEquals($expected, $actual);
219 | }
220 |
221 | public function testMsChapV1Packet()
222 | {
223 | $reqId = 135;
224 | $user = 'nemo';
225 | $pass = 'arctangent123$';
226 | $secret = 'xyzzy5461';
227 | $reqAuth = "\x09\x3e\x4a\xd1\x25\x39\x9f\x8a\xc4\xba\x6b\x00\xab\x69\xa0\x40";
228 | $nas = '127.0.0.1';
229 | $nasPort = 20;
230 | $challenge = "\x74\x0c\x79\x21\xe4\x5e\x91\x39";
231 |
232 | $client = new Radius();
233 | $client->setPacketType(Radius::TYPE_ACCESS_REQUEST)
234 | ->setNextIdentifier($reqId)
235 | ->setRequestAuthenticator($reqAuth)
236 | ->setSecret($secret)
237 | ->setUsername($user)
238 | ->setNasIPAddress($nas)
239 | ->setNasPort($nasPort)
240 | ->setAttribute(80, str_repeat("\x00", 16))
241 | ->setMsChapPassword($pass, $challenge);
242 |
243 | $packet = $client->generateRadiusPacket();
244 |
245 | $packet = bin2hex($packet);
246 | $expected = "01870082093e4ad125399f8ac4ba6b00ab69a04001066e656d6f04067f000001050600000014501248a3704ac91e8191497a1f3f213eb3381a10000001370b0a740c7921e45e91391a3a00000137013400010000000000000000000000000000000000000000000000004521bd46aebfd2ab3ec21dd6e6bbfa2e4ff325eab720fe37";
247 |
248 | $this->assertEquals($expected, $packet);
249 | }
250 |
251 | public function testEapPacketBasic()
252 | {
253 | $p = new MsChapV2Packet();
254 | $p->opcode = MsChapV2Packet::OPCODE_SUCCESS;
255 | $s = $p->__toString();
256 |
257 | $this->assertEquals("\x03", $s, "MsChapV2Packet success returns 0x03 without error");
258 |
259 | $p = new EAPPacket();
260 | $p->code = EAPPacket::CODE_REQUEST;
261 | $p->id = 111;
262 | $p->type = EAPPacket::TYPE_IDENTITY;
263 | $p->data = 'here is some data';
264 |
265 | $expected = "016f0016016865726520697320736f6d652064617461";
266 |
267 | $this->assertEquals($expected, bin2hex($p->__toString()));
268 |
269 | $parsed = EAPPacket::fromString($p->__toString());
270 |
271 | $this->assertEquals(EAPPacket::CODE_REQUEST, $parsed->code);
272 | $this->assertEquals(111, $parsed->id);
273 | $this->assertEquals(EAPPacket::TYPE_IDENTITY, $parsed->type);
274 | $this->assertEquals($p->data, $parsed->data);
275 |
276 | $p2 = new EAPPacket();
277 | $p2->code = EAPPacket::CODE_RESPONSE;
278 | $p2->id = 128;
279 | $p2->type = EAPPacket::TYPE_NOTIFICATION;
280 | $p2->data = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x99\x98\x97\x96\x95\x94\x93\x92\x91\x90";
281 |
282 | $p3 = EAPPacket::fromString($p2->__toString());
283 |
284 | $this->assertEquals(EAPPacket::CODE_RESPONSE, $p3->code);
285 | $this->assertEquals(128, $p3->id);
286 | $this->assertEquals(2, $p3->type);
287 | $this->assertEquals("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x99\x98\x97\x96\x95\x94\x93\x92\x91\x90", $p3->data);
288 | }
289 |
290 | public function testEapMsChapV2()
291 | {
292 | $server = getenv('RADIUS_SERVER_ADDR');
293 | $user = getenv('RADIUS_USER');
294 | $pass = getenv('RADIUS_PASS');
295 | $secret = getenv('RADIUS_SECRET');
296 |
297 | if (!$server) {
298 | $this->markTestSkipped('RADIUS_SERVER_ADDR environment variable not set');
299 | } elseif (!$user) {
300 | $this->markTestSkipped('RADIUS_USER environment variable not set');
301 | } elseif (!$pass) {
302 | $this->markTestSkipped('RADIUS_PASS environment variable not set');
303 | } elseif (!$secret) {
304 | $this->markTestSkipped('RADIUS_SECRET environment variable not set');
305 | }
306 |
307 | $client = new Radius();
308 | $client->setServer($server)
309 | ->setSecret($secret);
310 |
311 | $success = $client->accessRequestEapMsChapV2($user, $pass);
312 |
313 | $this->assertTrue($success);
314 | }
315 | }
316 |
--------------------------------------------------------------------------------
/lib/Pear_CHAP.php:
--------------------------------------------------------------------------------
1 |
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions
8 | are met:
9 |
10 | 1. Redistributions of source code must retain the above copyright
11 | notice, this list of conditions and the following disclaimer.
12 | 2. Redistributions in binary form must reproduce the above copyright
13 | notice, this list of conditions and the following disclaimer in the
14 | documentation and/or other materials provided with the distribution.
15 | 3. The names of the authors may not be used to endorse or promote products
16 | derived from this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
21 | IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
23 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
25 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
27 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
29 | This code cannot simply be copied and put under the GNU Public License or
30 | any other GPL-like (LGPL, GPL2) License.
31 |
32 | $Id: CHAP.php 302857 2010-08-28 21:12:59Z mbretter $
33 |
34 | This version of CHAP.php has been modified by Drew Phillips for dapphp/radius.
35 | Modifications remove the PEAR dependency, change from PHP4 OOP to PHP5, and
36 | mcrypt functions have been replaced with openssl_* functions.
37 |
38 | Changes are all commented inline throughout the source.
39 |
40 | $Id: Pear_CHAP.php 2.5.2 2018-01-25 03:30:29Z dapphp $
41 |
42 | */
43 |
44 | // require_once 'PEAR.php'; // removed for dapphp/radius
45 |
46 | /**
47 | * Classes for generating packets for various CHAP Protocols:
48 | * CHAP-MD5: RFC1994
49 | * MS-CHAPv1: RFC2433
50 | * MS-CHAPv2: RFC2759
51 | *
52 | * @package Crypt_CHAP
53 | * @author Michael Bretterklieber
54 | * @access public
55 | */
56 |
57 | /**
58 | * class Crypt_CHAP
59 | *
60 | * Abstract base class for CHAP
61 | *
62 | * @package Crypt_CHAP
63 | */
64 | class Crypt_CHAP /*extends PEAR // removed for dapphp/radius */
65 | {
66 | /**
67 | * Random binary challenge
68 | * @var string
69 | */
70 | public $challenge = null;
71 | //var $challenge = null; // removed for dapphp/radius
72 |
73 | /**
74 | * Binary response
75 | * @var string
76 | */
77 | public $response = null;
78 | //var $response = null; // removed for dapphp/radius
79 |
80 | /**
81 | * User password
82 | * @var string
83 | */
84 | public $password = null;
85 | //var $password = null; // removed for dapphp/radius
86 |
87 | /**
88 | * Id of the authentication request. Should incremented after every request.
89 | * @var integer
90 | */
91 | public $chapid = 1;
92 | //var $chapid = 1; // removed for dapphp/radius
93 |
94 | /**
95 | * Constructor
96 | *
97 | * Generates a random challenge
98 | * @return void
99 | */
100 | //function Crypt_CHAP() // removed for dapphp/radius
101 | public function __construct()
102 | {
103 | //$this->PEAR();
104 | $this->generateChallenge();
105 | }
106 |
107 | /**
108 | * Generates a random binary challenge
109 | *
110 | * @param string $varname Name of the property
111 | * @param integer $size Size of the challenge in Bytes
112 | * @return void
113 | */
114 | //function generateChallenge($varname = 'challenge', $size = 8) // removed for dapphp/radius
115 | public function generateChallenge($varname = 'challenge', $size = 8)
116 | {
117 | $this->$varname = '';
118 | for ($i = 0; $i < $size; $i++) {
119 | $this->$varname .= pack('C', 1 + mt_rand() % 255);
120 | }
121 | return $this->$varname;
122 | }
123 |
124 | /**
125 | * Generates the response. Overwrite this.
126 | *
127 | * @return void
128 | */
129 | //function challengeResponse() // removed for dapphp/radius
130 | public function challengeResponse()
131 | {
132 | }
133 |
134 | }
135 |
136 | /**
137 | * class Crypt_CHAP_MD5
138 | *
139 | * Generate CHAP-MD5 Packets
140 | *
141 | * @package Crypt_CHAP
142 | */
143 | class Crypt_CHAP_MD5 extends Crypt_CHAP
144 | {
145 |
146 | /**
147 | * Generates the response.
148 | *
149 | * CHAP-MD5 uses MD5-Hash for generating the response. The Hash consists
150 | * of the chapid, the plaintext password and the challenge.
151 | *
152 | * @return string
153 | */
154 | //function challengeResponse() // removed for dapphp/radius
155 | public function challengeResponse()
156 | {
157 | return pack('H*', md5(pack('C', $this->chapid) . $this->password . $this->challenge));
158 | }
159 | }
160 |
161 | /**
162 | * class Crypt_CHAP_MSv1
163 | *
164 | * Generate MS-CHAPv1 Packets. MS-CHAP doesen't use the plaintext password, it uses the
165 | * NT-HASH wich is stored in the SAM-Database or in the smbpasswd, if you are using samba.
166 | * The NT-HASH is MD4(str2unicode(plaintextpass)).
167 | * You need the hash extension for this class.
168 | *
169 | * @package Crypt_CHAP
170 | */
171 | class Crypt_CHAP_MSv1 extends Crypt_CHAP
172 | {
173 | /**
174 | * Wether using deprecated LM-Responses or not.
175 | * 0 = use LM-Response, 1 = use NT-Response
176 | * @var bool
177 | */
178 | protected $flags = 1;
179 | //var $flags = 1; // removed for dapphp/radius
180 |
181 | protected $useMcrypt = false; // added for dapphp/radius (php 5.3 must use mcrypt)
182 |
183 | /**
184 | * Constructor
185 | *
186 | * Loads the hash extension
187 | * @return void
188 | */
189 | //function Crypt_CHAP_MSv1() // removed for dapphp/radius
190 | public function __construct()
191 | {
192 | parent::__construct();
193 |
194 | // removed for dapphp/radius
195 | //$this->Crypt_CHAP();
196 | //$this->loadExtension('hash');
197 |
198 | // added openssl & mcrypt check for dapphp/radius
199 | if (!extension_loaded('openssl') && !extension_loaded('mcrypt')) {
200 | throw new \Exception("openssl and mcrypt are not installed; cannot use Radius MSCHAP functions");
201 | }
202 |
203 | // Added mcrypt check for PHP 5.3 for dapphp/radius
204 | // OPENSSL_RAW_DATA and OPENSSL_ZERO_PADDING are required but not
205 | // supported by ext/openssl until PHP 5.4.
206 | if (version_compare(PHP_VERSION, '5.4') < 0) {
207 | if (!extension_loaded('mcrypt')) {
208 | throw new \Exception("Radius MSCHAP functions require mcrypt extension for PHP 5.3");
209 | }
210 |
211 | $this->useMcrypt = true;
212 | }
213 | }
214 |
215 | /**
216 | * Generates the NT-HASH from the given plaintext password.
217 | *
218 | * @access public
219 | * @return string
220 | */
221 | //function ntPasswordHash($password = null) // removed for dapphp/radius
222 | public function ntPasswordHash($password = null)
223 | {
224 | //if (isset($password)) {
225 | if (!is_null($password)) {
226 | return pack('H*',hash('md4', $this->str2unicode($password)));
227 | } else {
228 | return pack('H*',hash('md4', $this->str2unicode($this->password)));
229 | }
230 | }
231 |
232 | /**
233 | * Converts ascii to unicode.
234 | *
235 | * @access public
236 | * @return string
237 | */
238 | //function str2unicode($str) // removed for dapphp/radius
239 | public function str2unicode($str)
240 | {
241 |
242 | if (function_exists('mb_convert_encoding')) {
243 | return mb_convert_encoding($str, 'UTF-16LE');
244 | } else {
245 | $uni = '';
246 | $str = (string) $str;
247 | for ($i = 0; $i < strlen($str); $i++) {
248 | $a = ord($str[$i]) << 8;
249 | $uni .= sprintf("%X", $a);
250 | }
251 | return pack('H*', $uni);
252 | }
253 | }
254 |
255 | /**
256 | * Generates the NT-Response.
257 | *
258 | * @access public
259 | * @return string
260 | */
261 | //function challengeResponse() // removed for dapphp/radius
262 | public function challengeResponse()
263 | {
264 | return $this->_challengeResponse();
265 | }
266 |
267 | /**
268 | * Generates the NT-Response.
269 | *
270 | * @access public
271 | * @return string
272 | */
273 | //function ntChallengeResponse() // removed for dapphp/radius
274 | public function ntChallengeResponse()
275 | {
276 | return $this->_challengeResponse(false);
277 | }
278 |
279 | /**
280 | * Generates the LAN-Manager-Response.
281 | *
282 | * @access public
283 | * @return string
284 | */
285 | //function lmChallengeResponse() // removed for dapphp/radius
286 | public function lmChallengeResponse()
287 | {
288 | return $this->_challengeResponse(true);
289 | }
290 |
291 | /**
292 | * Generates the response.
293 | *
294 | * Generates the response using DES.
295 | *
296 | * @param bool $lm wether generating LAN-Manager-Response
297 | * @access private
298 | * @return string
299 | */
300 | //function _challengeResponse($lm = false) // removed for dapphp/radius
301 | protected function _challengeResponse($lm = false)
302 | {
303 | if ($lm) {
304 | $hash = $this->lmPasswordHash();
305 | } else {
306 | $hash = $this->ntPasswordHash();
307 | }
308 |
309 | $hash = str_pad($hash, 21, "\0");
310 |
311 | if (extension_loaded('openssl') && $this->useMcrypt === false) {
312 | // added openssl routines for dapphp/radius
313 | $key = $this->_desAddParity(substr($hash, 0, 7));
314 | $resp1 = openssl_encrypt($this->challenge, 'des-ecb', $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING);
315 |
316 | $key = $this->_desAddParity(substr($hash, 7, 7));
317 | $resp2 = openssl_encrypt($this->challenge, 'des-ecb', $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING);
318 |
319 | $key = $this->_desAddParity(substr($hash, 14, 7));
320 | $resp3 = openssl_encrypt($this->challenge, 'des-ecb', $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING);
321 | } else {
322 | $td = mcrypt_module_open(MCRYPT_DES, '', MCRYPT_MODE_ECB, '');
323 | $iv = mcrypt_create_iv (mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
324 | $key = $this->_desAddParity(substr($hash, 0, 7));
325 | mcrypt_generic_init($td, $key, $iv);
326 | $resp1 = mcrypt_generic($td, $this->challenge);
327 | mcrypt_generic_deinit($td);
328 |
329 | $key = $this->_desAddParity(substr($hash, 7, 7));
330 | mcrypt_generic_init($td, $key, $iv);
331 | $resp2 = mcrypt_generic($td, $this->challenge);
332 | mcrypt_generic_deinit($td);
333 |
334 | $key = $this->_desAddParity(substr($hash, 14, 7));
335 | mcrypt_generic_init($td, $key, $iv);
336 | $resp3 = mcrypt_generic($td, $this->challenge);
337 | mcrypt_generic_deinit($td);
338 | mcrypt_module_close($td);
339 | }
340 |
341 | return $resp1 . $resp2 . $resp3;
342 | }
343 |
344 | /**
345 | * Generates the LAN-Manager-HASH from the given plaintext password.
346 | *
347 | * @access public
348 | * @return string
349 | */
350 | //function lmPasswordHash($password = null) // removed for dapphp/radius
351 | public function lmPasswordHash($password = null)
352 | {
353 | $plain = isset($password) ? $password : $this->password;
354 |
355 | $plain = substr(strtoupper($plain), 0, 14);
356 | while (strlen($plain) < 14) {
357 | $plain .= "\0";
358 | }
359 |
360 | return $this->_desHash(substr($plain, 0, 7)) . $this->_desHash(substr($plain, 7, 7));
361 | }
362 |
363 | /**
364 | * Generates an irreversible HASH.
365 | *
366 | * @access private
367 | * @return string
368 | */
369 | //function _desHash($plain) // removed for dapphp/radius
370 | private function _desHash($plain)
371 | {
372 | if (extension_loaded('openssl') && $this->useMcrypt === false) {
373 | // added openssl routines for dapphp/radius
374 | $key = $this->_desAddParity($plain);
375 | $hash = openssl_encrypt('KGS!@#$%', 'des-ecb', $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING);
376 |
377 | return $hash;
378 | } else {
379 | $key = $this->_desAddParity($plain);
380 | $td = mcrypt_module_open(MCRYPT_DES, '', MCRYPT_MODE_ECB, '');
381 | $iv = mcrypt_create_iv (mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
382 | mcrypt_generic_init($td, $key, $iv);
383 | $hash = mcrypt_generic($td, 'KGS!@#$%');
384 | mcrypt_generic_deinit($td);
385 | mcrypt_module_close($td);
386 |
387 | return $hash;
388 | }
389 | }
390 |
391 | /**
392 | * Adds the parity bit to the given DES key.
393 | *
394 | * @access private
395 | * @param string $key 7-Bytes Key without parity
396 | * @return string
397 | */
398 | //function _desAddParity($key) // removed for dapphp/radius
399 | protected function _desAddParity($key)
400 | {
401 | static $odd_parity = array(
402 | 1, 1, 2, 2, 4, 4, 7, 7, 8, 8, 11, 11, 13, 13, 14, 14,
403 | 16, 16, 19, 19, 21, 21, 22, 22, 25, 25, 26, 26, 28, 28, 31, 31,
404 | 32, 32, 35, 35, 37, 37, 38, 38, 41, 41, 42, 42, 44, 44, 47, 47,
405 | 49, 49, 50, 50, 52, 52, 55, 55, 56, 56, 59, 59, 61, 61, 62, 62,
406 | 64, 64, 67, 67, 69, 69, 70, 70, 73, 73, 74, 74, 76, 76, 79, 79,
407 | 81, 81, 82, 82, 84, 84, 87, 87, 88, 88, 91, 91, 93, 93, 94, 94,
408 | 97, 97, 98, 98,100,100,103,103,104,104,107,107,109,109,110,110,
409 | 112,112,115,115,117,117,118,118,121,121,122,122,124,124,127,127,
410 | 128,128,131,131,133,133,134,134,137,137,138,138,140,140,143,143,
411 | 145,145,146,146,148,148,151,151,152,152,155,155,157,157,158,158,
412 | 161,161,162,162,164,164,167,167,168,168,171,171,173,173,174,174,
413 | 176,176,179,179,181,181,182,182,185,185,186,186,188,188,191,191,
414 | 193,193,194,194,196,196,199,199,200,200,203,203,205,205,206,206,
415 | 208,208,211,211,213,213,214,214,217,217,218,218,220,220,223,223,
416 | 224,224,227,227,229,229,230,230,233,233,234,234,236,236,239,239,
417 | 241,241,242,242,244,244,247,247,248,248,251,251,253,253,254,254);
418 |
419 | $bin = '';
420 | for ($i = 0; $i < strlen($key); $i++) {
421 | $bin .= sprintf('%08s', decbin(ord($key[$i])));
422 | }
423 |
424 | $str1 = explode('-', substr(chunk_split($bin, 7, '-'), 0, -1));
425 | $x = '';
426 | foreach($str1 as $s) {
427 | $x .= sprintf('%02s', dechex($odd_parity[bindec($s . '0')]));
428 | }
429 |
430 | return pack('H*', $x);
431 |
432 | }
433 |
434 | /**
435 | * Generates the response-packet.
436 | *
437 | * @param bool $lm wether including LAN-Manager-Response
438 | * @access private
439 | * @return string
440 | */
441 | //function response($lm = false) // removed for dapphp/radius
442 | public function response($lm = false)
443 | {
444 | $ntresp = $this->ntChallengeResponse();
445 | if ($lm) {
446 | $lmresp = $this->lmChallengeResponse();
447 | } else {
448 | $lmresp = str_repeat ("\0", 24);
449 | }
450 |
451 | // Response: LM Response, NT Response, flags (0 = use LM Response, 1 = use NT Response)
452 | return $lmresp . $ntresp . pack('C', !$lm);
453 | }
454 | }
455 |
456 | /**
457 | * class Crypt_CHAP_MSv2
458 | *
459 | * Generate MS-CHAPv2 Packets. This version of MS-CHAP uses a 16 Bytes authenticator
460 | * challenge and a 16 Bytes peer Challenge. LAN-Manager responses no longer exists
461 | * in this version. The challenge is already a SHA1 challenge hash of both challenges
462 | * and of the username.
463 | *
464 | * @package Crypt_CHAP
465 | */
466 | class Crypt_CHAP_MSv2 extends Crypt_CHAP_MSv1
467 | {
468 | /**
469 | * The username
470 | * @var string
471 | */
472 | public $username = null;
473 | //var $username = null; // removed for dapphp/radius
474 |
475 | /**
476 | * The 16 Bytes random binary peer challenge
477 | * @var string
478 | */
479 | public $peerChallenge = null;
480 | //var $peerChallenge = null; // removed for dapphp/radius
481 |
482 | /**
483 | * The 16 Bytes random binary authenticator challenge
484 | * @var string
485 | */
486 | public $authChallenge = null;
487 | //var $authChallenge = null; // removed for dapphp/radius
488 |
489 | /**
490 | * Constructor
491 | *
492 | * Generates the 16 Bytes peer and authentication challenge
493 | * @return void
494 | */
495 | //function Crypt_CHAP_MSv2() // removed for dapphp/radius
496 | public function __construct()
497 | {
498 | //$this->Crypt_CHAP_MSv1(); // removed for dapphp/radius
499 | parent::__construct();
500 | $this->generateChallenge('peerChallenge', 16);
501 | $this->generateChallenge('authChallenge', 16);
502 | }
503 |
504 | /**
505 | * Generates a hash from the NT-HASH.
506 | *
507 | * @access public
508 | * @param string $nthash The NT-HASH
509 | * @return string
510 | */
511 | //function ntPasswordHashHash($nthash) // removed for dapphp/radius
512 | public function ntPasswordHashHash($nthash)
513 | {
514 | return pack('H*',hash('md4', $nthash));
515 | }
516 |
517 | /**
518 | * Generates the challenge hash from the peer and the authenticator challenge and
519 | * the username. SHA1 is used for this, but only the first 8 Bytes are used.
520 | *
521 | * @access public
522 | * @return string
523 | */
524 | //function challengeHash() // removed for dapphp/radius
525 | public function challengeHash()
526 | {
527 | return substr(pack('H*',hash('sha1', $this->peerChallenge . $this->authChallenge . $this->username)), 0, 8);
528 | }
529 |
530 | /**
531 | * Generates the response.
532 | *
533 | * @access public
534 | * @return string
535 | */
536 | //function challengeResponse() // removed for dapphp/radius
537 | public function challengeResponse()
538 | {
539 | $this->challenge = $this->challengeHash();
540 | return $this->_challengeResponse();
541 | }
542 |
543 | /**
544 | * Generates the encrypted new password.
545 | *
546 | * @access public
547 | * @param string $newPassword The new plain text password
548 | * @param string $oldPassword The old plain text password
549 | * @return string EncryptedPwBlock
550 | */
551 | public function newPasswordEncryptedWithOldNtPasswordHash($newPassword, $oldPassword)
552 | {
553 | $passwordHash = $this->ntPasswordHash($oldPassword);
554 | return $this->encryptPwBlockWithPasswordHash($this->str2unicode($newPassword), $passwordHash);
555 | }
556 |
557 | /**
558 | * Generates PwBlock
559 | *
560 | * @access public
561 | * @param string $password New password
562 | * @param string $passwordHash Old password hash
563 | * @return string PwBlock
564 | */
565 | public function encryptPwBlockWithPasswordHash($password, $passwordHash)
566 | {
567 | // [516=2*256+4] unicode(2) maxpasslength(256) passlength(4)
568 | $clearPwBlock = random_bytes(516);
569 | $pwSize = strlen($password);
570 | $pwOffset = strlen($clearPwBlock) - $pwSize - 4;
571 |
572 | $clearPwBlock = substr_replace($clearPwBlock, $password, $pwOffset, $pwSize);
573 |
574 | $clearPwBlock = substr_replace($clearPwBlock, pack("V", $pwSize), -4, 4);
575 |
576 | return $this->rc4($passwordHash, $clearPwBlock);
577 | }
578 |
579 | /**
580 | * RC4 symmetric cipher encryption/decryption
581 | *
582 | * @access public
583 | * @param string key - secret key for encryption/decryption
584 | * @param string str - string to be encrypted/decrypted
585 | * @return string
586 | */
587 | public function rc4($key, $str)
588 | {
589 | $s = array();
590 | for ($i = 0; $i < 256; $i++) {
591 | $s[$i] = $i;
592 | }
593 | $j = 0;
594 | for ($i = 0; $i < 256; $i++) {
595 | $j = ($j + $s[$i] + ord($key[$i % strlen($key)])) % 256;
596 | $x = $s[$i];
597 | $s[$i] = $s[$j];
598 | $s[$j] = $x;
599 | }
600 | $i = 0;
601 | $j = 0;
602 | $res = '';
603 | for ($y = 0; $y < strlen($str); $y++) {
604 | $i = ($i + 1) % 256;
605 | $j = ($j + $s[$i]) % 256;
606 | $x = $s[$i];
607 | $s[$i] = $s[$j];
608 | $s[$j] = $x;
609 | $res .= $str[$y] ^ chr($s[($s[$i] + $s[$j]) % 256]);
610 | }
611 | return $res;
612 | }
613 |
614 | /**
615 | * ?
616 | *
617 | * @access public
618 | * @param string $newPassword The new plain text password
619 | * @param string $oldPassword The old plain text password
620 | * @return string EncryptedPasswordHash
621 | */
622 | public function oldNtPasswordHashEncryptedWithNewNtPasswordHash($newPassword, $oldPassword)
623 | {
624 | $oldPasswordHash = $this->ntPasswordHash($oldPassword);
625 | $newPasswordHash = $this->ntPasswordHash($newPassword);
626 | return $this->ntPasswordHashEncryptedWithBlock($oldPasswordHash, $newPasswordHash);
627 | }
628 |
629 | /**
630 | * ?
631 | *
632 | * @access public
633 | * @param string $passwordHash Password hash to encrypt
634 | * @param string $block Key to use for encryption
635 | * @return string
636 | */
637 | public function ntPasswordHashEncryptedWithBlock($passwordHash, $block)
638 | {
639 | if (extension_loaded('openssl') && $this->useMcrypt === false) {
640 | $key = $this->_desAddParity(substr($block, 0, 7));
641 | $resp1 = openssl_encrypt(substr($passwordHash, 0, 8), 'des-ecb', $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING);
642 |
643 | $key = $this->_desAddParity(substr($block, 7, 7));
644 | $resp2 = openssl_encrypt(substr($passwordHash, 8, 8), 'des-ecb', $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING);
645 | } else {
646 | $td = mcrypt_module_open(MCRYPT_DES, '', MCRYPT_MODE_ECB, '');
647 | $iv = mcrypt_create_iv (mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
648 |
649 | $key = $this->_desAddParity(substr($block, 0, 7));
650 | mcrypt_generic_init($td, $key, $iv);
651 | $resp1 = mcrypt_generic($td, substr($passwordHash, 0, 8));
652 | mcrypt_generic_deinit($td);
653 |
654 | $key = $this->_desAddParity(substr($block, 7, 7));
655 | mcrypt_generic_init($td, $key, $iv);
656 | $resp2 = mcrypt_generic($td, substr($passwordHash, 8, 8));
657 | mcrypt_generic_deinit($td);
658 | }
659 | return $resp1 . $resp2;
660 | }
661 |
662 | }
663 |
--------------------------------------------------------------------------------
/src/Radius.php:
--------------------------------------------------------------------------------
1 | .
50 | *
51 | *
52 | * @author: SysCo/al
53 | * @author: Drew Phillips
54 | * @since CreationDate: 2008-01-04
55 | * @copyright (c) 2008 by SysCo systemes de communication sa
56 | * @copyright (c) 2016 by Drew Phillips
57 | * @version 2.5.1
58 | * @link http://developer.sysco.ch/php/
59 | * @link developer@sysco.ch
60 | * @link https://github.com/dapphp/radius
61 | * @link drew@drew-phillips.com
62 | */
63 |
64 | namespace Dapphp\Radius;
65 |
66 | /**
67 | * A pure PHP RADIUS client implementation.
68 | *
69 | * Originally created by SysCo/al based on radius.class.php v1.2.2
70 | * Modified for PHP5 & PHP7 compatibility by Drew Phillips
71 | * Switched from using ext/sockets to streams.
72 | *
73 | */
74 | class Radius
75 | {
76 | /** @var int Access-Request packet type identifier */
77 | const TYPE_ACCESS_REQUEST = 1;
78 |
79 | /** @var int Access-Accept packet type identifier */
80 | const TYPE_ACCESS_ACCEPT = 2;
81 |
82 | /** @var int Access-Reject packet type identifier */
83 | const TYPE_ACCESS_REJECT = 3;
84 |
85 | /** @var int Accounting-Request packet type identifier */
86 | const TYPE_ACCOUNTING_REQUEST = 4;
87 |
88 | /** @var int Accounting-Response packet type identifier */
89 | const TYPE_ACCOUNTING_RESPONSE = 5;
90 |
91 | /** @var int Access-Challenge packet type identifier */
92 | const TYPE_ACCESS_CHALLENGE = 11;
93 |
94 | /** @var int Reserved packet type */
95 | const TYPE_RESERVED = 255;
96 |
97 |
98 | /** @var string RADIUS server hostname or IP address */
99 | protected $server;
100 |
101 | /** @var string Shared secret with the RADIUS server */
102 | protected $secret;
103 |
104 | /** @var string RADIUS suffix (default is '') */
105 | protected $suffix;
106 |
107 | /** @var int Timeout for receiving UDP response packets (default = 5 seconds) */
108 | protected $timeout;
109 |
110 | /** @var int Authentication port (default = 1812) */
111 | protected $authenticationPort;
112 |
113 | /** @var int Accounting port (default = 1813) */
114 | protected $accountingPort;
115 |
116 | /** @var string Network Access Server (client) IP Address */
117 | protected $nasIpAddress;
118 |
119 | /** @var string NAS port. Physical port of the NAS authenticating the user */
120 | protected $nasPort;
121 |
122 | /** @var string Encrypted password, as described in RFC 2865 */
123 | protected $encryptedPassword;
124 |
125 | /** @var int Request-Authenticator, 16 octets random number */
126 | protected $requestAuthenticator;
127 |
128 | /** @var int Request-Authenticator from the response */
129 | protected $responseAuthenticator;
130 |
131 | /** @var string Username to send to the RADIUS server */
132 | protected $username;
133 |
134 | /** @var string Password for authenticating with the RADIUS server (before encryption) */
135 | protected $password;
136 |
137 | /** @var int The CHAP identifier for CHAP-Password attributes */
138 | protected $chapIdentifier;
139 |
140 | /** @var string Identifier field for the packet to be sent */
141 | protected $identifierToSend;
142 |
143 | /** @var string Identifier field for the received packet */
144 | protected $identifierReceived;
145 |
146 | /** @var int RADIUS packet type (1=Access-Request, 2=Access-Accept, etc) */
147 | protected $radiusPacket;
148 |
149 | /** @var int Packet type received in response from RADIUS server */
150 | protected $radiusPacketReceived;
151 |
152 | /** @var array List of RADIUS attributes to send */
153 | protected $attributesToSend;
154 |
155 | /** @var array List of attributes received in response */
156 | protected $attributesReceived;
157 |
158 | /** @var bool Whether or not to enable debug output */
159 | protected $debug;
160 |
161 | /** @var array RADIUS attributes info array */
162 | protected $attributesInfo;
163 |
164 | /** @var array RADIUS packet codes info array */
165 | protected $radiusPackets;
166 |
167 | /** @var int The error code from the last operation */
168 | protected $errorCode;
169 |
170 | /** @var string The error message from the last operation */
171 | protected $errorMessage;
172 |
173 |
174 | /**
175 | * Radius constructor.
176 | *
177 | * @param string $radiusHost The RADIUS server hostname or IP address
178 | * @param string $sharedSecret The RADIUS server shared secret
179 | * @param string $radiusSuffix The username suffix to use when authenticating
180 | * @param int $timeout The timeout (in seconds) to wait for RADIUS responses
181 | * @param int $authenticationPort The port for authentication requests (default = 1812)
182 | * @param int $accountingPort The port for accounting requests (default = 1813)
183 | */
184 | public function __construct($radiusHost = '127.0.0.1',
185 | $sharedSecret = '',
186 | $radiusSuffix = '',
187 | $timeout = 5,
188 | $authenticationPort = 1812,
189 | $accountingPort = 1813)
190 | {
191 | $this->radiusPackets = array();
192 | $this->radiusPackets[1] = 'Access-Request';
193 | $this->radiusPackets[2] = 'Access-Accept';
194 | $this->radiusPackets[3] = 'Access-Reject';
195 | $this->radiusPackets[4] = 'Accounting-Request';
196 | $this->radiusPackets[5] = 'Accounting-Response';
197 | $this->radiusPackets[11] = 'Access-Challenge';
198 | $this->radiusPackets[12] = 'Status-Server (experimental)';
199 | $this->radiusPackets[13] = 'Status-Client (experimental)';
200 | $this->radiusPackets[255] = 'Reserved';
201 |
202 | $this->attributesInfo = array();
203 | $this->attributesInfo[1] = array('User-Name', 'S');
204 | $this->attributesInfo[2] = array('User-Password', 'S');
205 | $this->attributesInfo[3] = array('CHAP-Password', 'S'); // Type (1) / Length (1) / CHAP Ident (1) / String
206 | $this->attributesInfo[4] = array('NAS-IP-Address', 'A');
207 | $this->attributesInfo[5] = array('NAS-Port', 'I');
208 | $this->attributesInfo[6] = array('Service-Type', 'I');
209 | $this->attributesInfo[7] = array('Framed-Protocol', 'I');
210 | $this->attributesInfo[8] = array('Framed-IP-Address', 'A');
211 | $this->attributesInfo[9] = array('Framed-IP-Netmask', 'A');
212 | $this->attributesInfo[10] = array('Framed-Routing', 'I');
213 | $this->attributesInfo[11] = array('Filter-Id', 'T');
214 | $this->attributesInfo[12] = array('Framed-MTU', 'I');
215 | $this->attributesInfo[13] = array('Framed-Compression', 'I');
216 | $this->attributesInfo[14] = array('Login-IP-Host', 'A');
217 | $this->attributesInfo[15] = array('Login-service', 'I');
218 | $this->attributesInfo[16] = array('Login-TCP-Port', 'I');
219 | $this->attributesInfo[17] = array('(unassigned)', '');
220 | $this->attributesInfo[18] = array('Reply-Message', 'T');
221 | $this->attributesInfo[19] = array('Callback-Number', 'S');
222 | $this->attributesInfo[20] = array('Callback-Id', 'S');
223 | $this->attributesInfo[21] = array('(unassigned)', '');
224 | $this->attributesInfo[22] = array('Framed-Route', 'T');
225 | $this->attributesInfo[23] = array('Framed-IPX-Network', 'I');
226 | $this->attributesInfo[24] = array('State', 'S');
227 | $this->attributesInfo[25] = array('Class', 'S');
228 | $this->attributesInfo[26] = array('Vendor-Specific', 'S'); // Type (1) / Length (1) / Vendor-Id (4) / Vendor type (1) / Vendor length (1) / Attribute-Specific...
229 | $this->attributesInfo[27] = array('Session-Timeout', 'I');
230 | $this->attributesInfo[28] = array('Idle-Timeout', 'I');
231 | $this->attributesInfo[29] = array('Termination-Action', 'I');
232 | $this->attributesInfo[30] = array('Called-Station-Id', 'S');
233 | $this->attributesInfo[31] = array('Calling-Station-Id', 'S');
234 | $this->attributesInfo[32] = array('NAS-Identifier', 'S');
235 | $this->attributesInfo[33] = array('Proxy-State', 'S');
236 | $this->attributesInfo[34] = array('Login-LAT-Service', 'S');
237 | $this->attributesInfo[35] = array('Login-LAT-Node', 'S');
238 | $this->attributesInfo[36] = array('Login-LAT-Group', 'S');
239 | $this->attributesInfo[37] = array('Framed-AppleTalk-Link', 'I');
240 | $this->attributesInfo[38] = array('Framed-AppleTalk-Network', 'I');
241 | $this->attributesInfo[39] = array('Framed-AppleTalk-Zone', 'S');
242 | $this->attributesInfo[60] = array('CHAP-Challenge', 'S');
243 | $this->attributesInfo[61] = array('NAS-Port-Type', 'I');
244 | $this->attributesInfo[62] = array('Port-Limit', 'I');
245 | $this->attributesInfo[63] = array('Login-LAT-Port', 'S');
246 | $this->attributesInfo[76] = array('Prompt', 'I');
247 | $this->attributesInfo[79] = array('EAP-Message', 'S');
248 | $this->attributesInfo[80] = array('Message-Authenticator', 'S');
249 |
250 | $this->identifierToSend = -1;
251 | $this->chapIdentifier = 1;
252 |
253 | $this->generateRequestAuthenticator()
254 | ->setServer($radiusHost)
255 | ->setSecret($sharedSecret)
256 | ->setAuthenticationPort($authenticationPort)
257 | ->setAccountingPort($accountingPort)
258 | ->setTimeout($timeout)
259 | ->setRadiusSuffix($radiusSuffix);
260 |
261 | $this->clearError()
262 | ->clearDataToSend()
263 | ->clearDataReceived();
264 | }
265 |
266 | /**
267 | * Returns a string of the last error message and code, if any.
268 | *
269 | * @return string The last error message and code, or an empty string if no error set.
270 | */
271 | public function getLastError()
272 | {
273 | if (0 < $this->errorCode) {
274 | return $this->errorMessage.' ('.$this->errorCode.')';
275 | } else {
276 | return '';
277 | }
278 | }
279 |
280 | /**
281 | * Get the code of the last error.
282 | *
283 | * @return int The error code
284 | */
285 | public function getErrorCode()
286 | {
287 | return $this->errorCode;
288 | }
289 |
290 | /**
291 | * Get the message of the last error.
292 | *
293 | * @return string The last error message
294 | */
295 | public function getErrorMessage()
296 | {
297 | return $this->errorMessage;
298 | }
299 |
300 | /**
301 | * Enable or disable debug (console) output.
302 | *
303 | * @param bool $enabled boolean true to enable debugging, anything else to disable it.
304 | *
305 | * @return self
306 | */
307 | public function setDebug($enabled = true)
308 | {
309 | $this->debug = (true === $enabled);
310 | return $this;
311 | }
312 |
313 | /**
314 | * Set the hostname or IP address of the RADIUS server to send requests to.
315 | *
316 | * @param string $hostOrIp The hostname or IP address of the RADIUS server
317 | * @return self
318 | */
319 | public function setServer($hostOrIp)
320 | {
321 | $this->server = gethostbyname($hostOrIp);
322 | return $this;
323 | }
324 |
325 | /**
326 | * Set the RADIUS shared secret between the client and RADIUS server.
327 | *
328 | * @param string $secret The shared secret
329 | * @return self
330 | */
331 | public function setSecret($secret)
332 | {
333 | $this->secret = $secret;
334 | return $this;
335 | }
336 |
337 | /**
338 | * Gets the currently set RADIUS shared secret.
339 | *
340 | * @return string The shared secret
341 | */
342 | public function getSecret()
343 | {
344 | return $this->secret;
345 | }
346 |
347 | /**
348 | * Set the username suffix for authentication (e.g. '.ppp').
349 | * This must be set before setting the username.
350 | *
351 | * @param string $suffix The RADIUS user suffix (e.g. .ppp)
352 | * @return self
353 | */
354 | public function setRadiusSuffix($suffix)
355 | {
356 | $this->suffix = $suffix;
357 | return $this;
358 | }
359 |
360 | /**
361 | * Set the username to authenticate as with the RADIUS server.
362 | * If the username does not contain the '@' character, then the RADIUS suffix
363 | * will be appended to the username.
364 | *
365 | * @param string $username The username for authentication
366 | * @return self
367 | */
368 | public function setUsername($username = '')
369 | {
370 | if (false === strpos($username, '@'))
371 | {
372 | $username .= $this->suffix;
373 | }
374 |
375 | $this->username = $username;
376 | $this->setAttribute(1, $this->username);
377 |
378 | return $this;
379 | }
380 |
381 | /**
382 | * Get the authentication username for RADIUS requests.
383 | *
384 | * @return string The username for authentication
385 | */
386 | public function getUsername()
387 | {
388 | return $this->username;
389 | }
390 |
391 | /**
392 | * Set the User-Password for PAP authentication.
393 | * Do not use this if you will be using CHAP-MD5, MS-CHAP v1 or MS-CHAP v2 passwords.
394 | *
395 | * @param string $password The plain text password for authentication
396 | * @return self
397 | */
398 | public function setPassword($password)
399 | {
400 | $this->password = $password;
401 | $encryptedPassword = $this->getEncryptedPassword($password, $this->getSecret(), $this->getRequestAuthenticator());
402 |
403 | $this->setAttribute(2, $encryptedPassword);
404 |
405 | return $this;
406 | }
407 |
408 | /**
409 | * Get the plaintext password for authentication.
410 | *
411 | * @return string The authentication password
412 | */
413 | public function getPassword()
414 | {
415 | return $this->password;
416 | }
417 |
418 | /**
419 | * Get a RADIUS encrypted password from a plaintext password, shared secret, and request authenticator.
420 | * This method should generally not need to be called directly.
421 | *
422 | * @param string $password The plain text password
423 | * @param string $secret The RADIUS shared secret
424 | * @param string $requestAuthenticator 16 byte request authenticator
425 | * @return string The encrypted password
426 | */
427 | public function getEncryptedPassword($password, $secret, $requestAuthenticator)
428 | {
429 | $encryptedPassword = '';
430 | $paddedPassword = $password;
431 |
432 | if (0 != (strlen($password) % 16)) {
433 | $paddedPassword .= str_repeat(chr(0), (16 - strlen($password) % 16));
434 | }
435 |
436 | $previous = $requestAuthenticator;
437 |
438 | for ($i = 0; $i < (strlen($paddedPassword) / 16); ++$i) {
439 | $temp = md5($secret . $previous);
440 |
441 | $previous = '';
442 | for ($j = 0; $j <= 15; ++$j) {
443 | $value1 = ord(substr($paddedPassword, ($i * 16) + $j, 1));
444 | $value2 = hexdec(substr($temp, 2 * $j, 2));
445 | $xor_result = $value1 ^ $value2;
446 | $previous .= chr($xor_result);
447 | }
448 | $encryptedPassword .= $previous;
449 | }
450 |
451 | return $encryptedPassword;
452 | }
453 |
454 | /**
455 | * Set whether a Message-Authenticator attribute (80) should be included in the request.
456 | * Note: Some servers (e.g. Microsoft NPS) may be configured to require all packets contain this.
457 | *
458 | * @param bool $include Boolean true to include in packets, false otherwise
459 | * @return self
460 | */
461 | public function setIncludeMessageAuthenticator($include = true)
462 | {
463 | if ($include) {
464 | $this->setAttribute(80, str_repeat("\x00", 16));
465 | } else {
466 | $this->removeAttribute(80);
467 | }
468 |
469 | return $this;
470 | }
471 |
472 | /**
473 | * Sets the next sequence number that will be used when sending packets.
474 | * There is generally no need to call this method directly.
475 | *
476 | * @param int $nextId The CHAP packet identifier number
477 | * @return self
478 | */
479 | public function setChapId($nextId)
480 | {
481 | $this->chapIdentifier = (int)$nextId;
482 |
483 | return $this;
484 | }
485 |
486 | /**
487 | * Get the CHAP ID and increment the counter.
488 | *
489 | * @return int The CHAP identifier for the next packet
490 | */
491 | public function getChapId()
492 | {
493 | $id = $this->chapIdentifier;
494 | $this->chapIdentifier++;
495 |
496 | return $id;
497 | }
498 |
499 | /**
500 | * Set the CHAP password (for CHAP authentication).
501 | *
502 | * @param string $password The plaintext password to hash using CHAP.
503 | * @return self
504 | */
505 | public function setChapPassword($password)
506 | {
507 | $chapId = $this->getChapId();
508 | $chapMd5 = $this->getChapPassword($password, $chapId, $this->getRequestAuthenticator());
509 |
510 | $this->setAttribute(3, pack('C', $chapId) . $chapMd5);
511 |
512 | return $this;
513 | }
514 |
515 | /**
516 | * Generate a CHAP password. There is generally no need to call this method directly.
517 | *
518 | * @param string $password The password to hash using CHAP
519 | * @param int $chapId The CHAP packet ID
520 | * @param string $requestAuthenticator The request authenticator value
521 | * @return string The hashed CHAP password
522 | */
523 | public function getChapPassword($password, $chapId, $requestAuthenticator)
524 | {
525 | return md5(pack('C', $chapId) . $password . $requestAuthenticator, true);
526 | }
527 |
528 | /**
529 | * Set the MS-CHAP password in the RADIUS packet (for authentication using MS-CHAP passwords)
530 | *
531 | * @param string $password The plaintext password
532 | * @param string $challenge The CHAP challenge
533 | * @return self
534 | */
535 | public function setMsChapPassword($password, $challenge = null)
536 | {
537 | $chap = new \Crypt_CHAP_MSv1();
538 | $chap->chapid = mt_rand(1, 255);
539 | $chap->password = $password;
540 | if (is_null($challenge)) {
541 | $chap->generateChallenge();
542 | } else {
543 | $chap->challenge = $challenge;
544 | }
545 |
546 | $response = "\x00\x01" . str_repeat ("\0", 24) . $chap->ntChallengeResponse();
547 |
548 | $this->setIncludeMessageAuthenticator();
549 | $this->setVendorSpecificAttribute(VendorId::MICROSOFT, 11, $chap->challenge);
550 | $this->setVendorSpecificAttribute(VendorId::MICROSOFT, 1, $response);
551 |
552 | return $this;
553 | }
554 |
555 | /**
556 | * Sets the Network Access Server (NAS) IP address (the RADIUS client IP).
557 | *
558 | * @param string $hostOrIp The hostname or IP address of the RADIUS client
559 | * @return self
560 | */
561 | public function setNasIPAddress($hostOrIp = '')
562 | {
563 | if (0 < strlen($hostOrIp)) {
564 | $this->nasIpAddress = gethostbyname($hostOrIp);
565 | } else {
566 | $hostOrIp = @php_uname('n');
567 | if (empty($hostOrIp)) {
568 | $hostOrIp = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
569 | }
570 | if (empty($hostOrIp)) {
571 | $hostOrIp = (isset($_SERVER['SERVER_ADDR'])) ? $_SERVER['SERVER_ADDR'] : '0.0.0.0';
572 | }
573 |
574 | $this->nasIpAddress = gethostbyname($hostOrIp);
575 | }
576 |
577 | $this->setAttribute(4, $this->nasIpAddress);
578 |
579 | return $this;
580 | }
581 |
582 | /**
583 | * Get the currently set NAS IP address
584 | *
585 | * @return string The NAS hostname or IP
586 | */
587 | public function getNasIPAddress()
588 | {
589 | return $this->nasIpAddress;
590 | }
591 |
592 | /**
593 | * Set the physical port number of the NAS which is authenticating the user.
594 | *
595 | * @param int $port The NAS port
596 | * @return self
597 | */
598 | public function setNasPort($port = 0)
599 | {
600 | $this->nasPort = intval($port);
601 | $this->setAttribute(5, $this->nasPort);
602 |
603 | return $this;
604 | }
605 |
606 | /**
607 | * Get the NAS port attribute
608 | *
609 | * @return string
610 | */
611 | public function getNasPort()
612 | {
613 | return $this->nasPort;
614 | }
615 |
616 | /**
617 | * Set the timeout (in seconds) after which we'll give up waiting for a response from the RADIUS server.
618 | *
619 | * @param int $timeout The timeout (in seconds) for waiting for RADIUS responses.
620 | * @return self
621 | */
622 | public function setTimeout($timeout = 5)
623 | {
624 | if (intval($timeout) > 0) {
625 | $this->timeout = intval($timeout);
626 | }
627 |
628 | return $this;
629 | }
630 |
631 | /**
632 | * Get the current timeout value for RADIUS response packets.
633 | *
634 | * @return int The timeout
635 | */
636 | public function getTimeout()
637 | {
638 | return $this->timeout;
639 | }
640 |
641 | /**
642 | * Set the port number used by the RADIUS server for authentication (default = 1812).
643 | *
644 | * @param int $port The port for sending Access-Request packets
645 | * @return self
646 | */
647 | public function setAuthenticationPort($port)
648 | {
649 | if ((intval($port) > 0) && (intval($port) < 65536)) {
650 | $this->authenticationPort = intval($port);
651 | }
652 |
653 | return $this;
654 | }
655 |
656 | /**
657 | * Get the port number used for authentication
658 | *
659 | * @return int The RADIUS auth port
660 | */
661 | public function getAuthenticationPort()
662 | {
663 | return $this->authenticationPort;
664 | }
665 |
666 | /**
667 | * Set the port number used by the RADIUS server for accounting (default = 1813)
668 | *
669 | * @param int $port The port for sending Accounting request packets
670 | * @return self
671 | */
672 | public function setAccountingPort($port)
673 | {
674 | if ((intval($port) > 0) && (intval($port) < 65536))
675 | {
676 | $this->accountingPort = intval($port);
677 | }
678 |
679 | return $this;
680 | }
681 |
682 | /**
683 | * Returns the raw wire data of the last received RADIUS packet.
684 | *
685 | * @return string The raw packet data of the last RADIUS response
686 | */
687 | public function getResponsePacket()
688 | {
689 | return $this->radiusPacketReceived;
690 | }
691 |
692 | /**
693 | * Alias of Radius::getAttribute()
694 | *
695 | * @param int $type The attribute ID to get
696 | * @return NULL|string NULL if no such attribute was set in the response packet, or the data of that attribute
697 | */
698 | public function getReceivedAttribute($type)
699 | {
700 | return $this->getAttribute($type);
701 | }
702 |
703 | /**
704 | * Returns an array of all attributes from the last received RADIUS packet.
705 | *
706 | * @return array Array of received attributes. Each entry is an array with $attr[0] = attribute ID, $attr[1] = data
707 | */
708 | public function getReceivedAttributes()
709 | {
710 | return $this->attributesReceived;
711 | }
712 |
713 | /**
714 | * For debugging purposes. Print the attributes from the last received packet as a readable string
715 | *
716 | * @return string The RADIUS packet attributes in human readable format
717 | */
718 | public function getReadableReceivedAttributes()
719 | {
720 | $attributes = '';
721 |
722 | if (isset($this->attributesReceived)) {
723 | foreach($this->attributesReceived as $receivedAttr) {
724 | $info = $this->getAttributesInfo($receivedAttr[0]);
725 | $attributes .= sprintf('%s: ', $info[0]);
726 |
727 | if (26 == $receivedAttr[0]) {
728 | $vendorArr = $this->decodeVendorSpecificContent($receivedAttr[1]);
729 | foreach($vendorArr as $vendor) {
730 | $attributes .= sprintf('Vendor-Id: %s, Vendor-type: %s, Attribute-specific: %s',
731 | $vendor[0], $vendor[1], $vendor[2]);
732 | }
733 | } else {
734 | $attributes = $receivedAttr[1];
735 | }
736 |
737 | $attributes .= "
\n";
738 | }
739 | }
740 |
741 | return $attributes;
742 | }
743 |
744 | /**
745 | * Get the value of an attribute from the last received RADIUS response packet.
746 | *
747 | * @param int $type The attribute ID to get
748 | * @return NULL|string NULL if no such attribute was set in the response packet, or the data of that attribute
749 | */
750 | public function getAttribute($type)
751 | {
752 | $value = null;
753 |
754 | if (is_array($this->attributesReceived)) {
755 | foreach($this->attributesReceived as $attr) {
756 | if (intval($type) == $attr[0]) {
757 | $value = $attr[1];
758 | break;
759 | }
760 | }
761 | }
762 |
763 | return $value;
764 | }
765 |
766 | /**
767 | * Gets the name of a RADIUS packet from the numeric value.
768 | * This is only used for debugging functions
769 | *
770 | * @param int $info_index The packet type number
771 | * @return mixed|string
772 | */
773 | public function getRadiusPacketInfo($info_index)
774 | {
775 | if (isset($this->radiusPackets[intval($info_index)])) {
776 | return $this->radiusPackets[intval($info_index)];
777 | } else {
778 | return '';
779 | }
780 | }
781 |
782 | /**
783 | * Gets the info about a RADIUS attribute identifier such as the attribute name and data type.
784 | * This is used internally for encoding packets and debug output.
785 | *
786 | * @param int $info_index The RADIUS packet attribute number
787 | * @return array 2 element array with Attribute-Name and Data Type
788 | */
789 | public function getAttributesInfo($info_index)
790 | {
791 | if (isset($this->attributesInfo[intval($info_index)])) {
792 | return $this->attributesInfo[intval($info_index)];
793 | } else {
794 | return array('', '');
795 | }
796 | }
797 |
798 | /**
799 | * Set an arbitrary RADIUS attribute to be sent in the next packet.
800 | *
801 | * @param int $type The number of the RADIUS attribute
802 | * @param mixed $value The value of the attribute
803 | * @return self
804 | */
805 | public function setAttribute($type, $value)
806 | {
807 | $index = -1;
808 | if (is_array($this->attributesToSend)) {
809 | foreach($this->attributesToSend as $i => $attr) {
810 | if (is_array($attr)) {
811 | $tmp = $attr[0];
812 | } else {
813 | $tmp = $attr;
814 | }
815 | if ($type == ord(substr($tmp, 0, 1))) {
816 | $index = $i;
817 | break;
818 | }
819 | }
820 | }
821 |
822 | $temp = null;
823 |
824 | if (isset($this->attributesInfo[$type])) {
825 | switch ($this->attributesInfo[$type][1]) {
826 | case 'T':
827 | // Text, 1-253 octets containing UTF-8 encoded ISO 10646 characters (RFC 2279).
828 | $temp = chr($type) . chr(2 + strlen($value)) . $value;
829 | break;
830 | case 'S':
831 | // String, 1-253 octets containing binary data (values 0 through 255 decimal, inclusive).
832 | $temp = chr($type) . chr(2 + strlen($value)) . $value;
833 | break;
834 | case 'A':
835 | // Address, 32 bit value, most significant octet first.
836 | $ip = explode('.', $value);
837 | $temp = chr($type) . chr(6) . chr($ip[0]) . chr($ip[1]) . chr($ip[2]) . chr($ip[3]);
838 | break;
839 | case 'I':
840 | // Integer, 32 bit unsigned value, most significant octet first.
841 | $temp = chr($type) . chr(6) .
842 | chr(intval(($value / (256 * 256 * 256))) % 256) .
843 | chr(intval(($value / (256 * 256))) % 256) .
844 | chr(intval(($value / (256))) % 256) .
845 | chr($value % 256);
846 | break;
847 | case 'D':
848 | // Time, 32 bit unsigned value, most significant octet first -- seconds since 00:00:00 UTC, January 1, 1970. (not used in this RFC)
849 | $temp = null;
850 | break;
851 | default:
852 | $temp = null;
853 | }
854 | }
855 |
856 | $multiAVP = array(26, 79); // vendor specific and EAP-Message
857 | if ($index > -1) {
858 | if (in_array($type, $multiAVP)) {
859 | $this->attributesToSend[$index][] = $temp;
860 | $action = 'Added';
861 | } else {
862 | $this->attributesToSend[$index] = $temp;
863 | $action = 'Modified';
864 | }
865 | } else {
866 | $this->attributesToSend[] = (in_array($type, $multiAVP)) ? array($temp) : $temp;
867 | $action = 'Added';
868 | }
869 |
870 | $info = $this->getAttributesInfo($type);
871 | $this->debugInfo("{$action} Attribute {$type} ({$info[0]}), format {$info[1]}, value {$value}");
872 |
873 | return $this;
874 | }
875 |
876 | /**
877 | * Get one or all set attributes to send
878 | *
879 | * @param int|null $type RADIUS attribute type, or null for all
880 | * @return mixed array of attributes to send, or null if specific attribute not found, or
881 | */
882 | public function getAttributesToSend($type = null)
883 | {
884 | if (is_array($this->attributesToSend)) {
885 | if ($type == null) {
886 | return $this->attributesToSend;
887 | } else {
888 | foreach($this->attributesToSend as $i => $attr) {
889 | if (is_array($attr)) {
890 | $tmp = $attr[0];
891 | } else {
892 | $tmp = $attr;
893 | }
894 | if ($type == ord(substr($tmp, 0, 1))) {
895 | return $this->decodeAttribute(substr($tmp, 2), $type);
896 | }
897 | }
898 | return null;
899 | }
900 | }
901 |
902 | return array();
903 | }
904 |
905 | /**
906 | * Adds a vendor specific attribute to the RADIUS packet
907 | *
908 | * @param int $vendorId The RADIUS vendor ID
909 | * @param int $attributeType The attribute number of the vendor specific attribute
910 | * @param mixed $attributeValue The data for the attribute
911 | * @return self
912 | */
913 | public function setVendorSpecificAttribute($vendorId, $attributeType, $attributeValue)
914 | {
915 | $data = pack('N', $vendorId);
916 | $data .= chr($attributeType);
917 | $data .= chr(2 + strlen($attributeValue));
918 | $data .= $attributeValue;
919 |
920 | $this->setAttribute(26, $data);
921 |
922 | return $this;
923 | }
924 |
925 | /**
926 | * Remove an attribute from a RADIUS packet
927 | *
928 | * @param int $type The attribute number to remove
929 | * @return self
930 | */
931 | public function removeAttribute($type)
932 | {
933 | if (is_array($this->attributesToSend)) {
934 | foreach($this->attributesToSend as $i => $attr) {
935 | if (is_array($attr)) {
936 | $tmp = $attr[0];
937 | } else {
938 | $tmp = $attr;
939 | }
940 | if ($type == ord(substr($tmp, 0, 1))) {
941 | unset($this->attributesToSend[$i]);
942 | break;
943 | }
944 | }
945 | }
946 |
947 | return $this;
948 | }
949 |
950 | /**
951 | * Clear all attributes to send so the next packet contains no attributes except ones added after calling this function.
952 | *
953 | * @return self
954 | */
955 | public function resetAttributes()
956 | {
957 | $this->attributesToSend = null;
958 | return $this;
959 | }
960 |
961 | /**
962 | * Remove vendor specific attributes from the request.
963 | *
964 | * @return self
965 | */
966 | public function resetVendorSpecificAttributes()
967 | {
968 | $this->removeAttribute(26);
969 |
970 | return $this;
971 | }
972 |
973 | /**
974 | * Decodes a vendor specific attribute in a response packet
975 | *
976 | * @param string $rawValue The raw packet attribute data as seen on the wire
977 | * @return array Array of vendor specific attributes in the response packet
978 | */
979 | public function decodeVendorSpecificContent($rawValue)
980 | {
981 | $result = array();
982 | $offset = 0;
983 | $vendorId = (ord(substr($rawValue, 0, 1)) * 256 * 256 * 256) +
984 | (ord(substr($rawValue, 1, 1)) * 256 * 256) +
985 | (ord(substr($rawValue, 2, 1)) * 256) +
986 | ord(substr($rawValue, 3, 1));
987 |
988 | $offset += 4;
989 | while ($offset < strlen($rawValue)) {
990 | $vendorType = (ord(substr($rawValue, 0 + $offset, 1)));
991 | $vendorLength = (ord(substr($rawValue, 1 + $offset, 1)));
992 | $attributeSpecific = substr($rawValue, 2 + $offset, $vendorLength);
993 | $result[] = array($vendorId, $vendorType, $attributeSpecific);
994 | $offset += $vendorLength;
995 | }
996 |
997 | return $result;
998 | }
999 |
1000 | /**
1001 | * Issue an Access-Request packet to the RADIUS server.
1002 | *
1003 | * @param string $username Username to authenticate as
1004 | * @param string $password Password to authenticate with using PAP
1005 | * @param int $timeout The timeout (in seconds) to wait for a response packet
1006 | * @param string $state The state of the request (default is Service-Type=1)
1007 | * @return boolean true if the server sent an Access-Accept packet, false otherwise
1008 | */
1009 | public function accessRequest($username = '', $password = '', $timeout = 0, $state = null)
1010 | {
1011 | $this->clearDataReceived()
1012 | ->clearError()
1013 | ->setPacketType(self::TYPE_ACCESS_REQUEST);
1014 |
1015 | if (0 < strlen($username)) {
1016 | $this->setUsername($username);
1017 | }
1018 |
1019 | if (0 < strlen($password)) {
1020 | $this->setPassword($password);
1021 | }
1022 |
1023 | if ($state !== null) {
1024 | $this->setAttribute(24, $state);
1025 | } else {
1026 | $this->setAttribute(6, 1); // 1=Login
1027 | }
1028 |
1029 | if (intval($timeout) > 0) {
1030 | $this->setTimeout($timeout);
1031 | }
1032 |
1033 | $packetData = $this->generateRadiusPacket();
1034 |
1035 | $conn = $this->sendRadiusRequest($packetData);
1036 | if (!$conn) {
1037 | $this->debugInfo(sprintf(
1038 | 'Failed to send packet to %s; error: %s',
1039 | $this->server,
1040 | $this->getErrorMessage())
1041 | );
1042 |
1043 | return false;
1044 | }
1045 |
1046 | $receivedPacket = $this->readRadiusResponse($conn);
1047 | @fclose($conn);
1048 |
1049 | if (!$receivedPacket) {
1050 | $this->debugInfo(sprintf(
1051 | 'Error receiving response packet from %s; error: %s',
1052 | $this->server,
1053 | $this->getErrorMessage())
1054 | );
1055 |
1056 | return false;
1057 | }
1058 |
1059 | if (!$this->parseRadiusResponsePacket($receivedPacket)) {
1060 | $this->debugInfo(sprintf(
1061 | 'Bad RADIUS response from %s; error: %s',
1062 | $this->server,
1063 | $this->getErrorMessage())
1064 | );
1065 |
1066 | return false;
1067 | }
1068 |
1069 | if ($this->radiusPacketReceived == self::TYPE_ACCESS_REJECT) {
1070 | $this->errorCode = 3;
1071 | $this->errorMessage = 'Access rejected';
1072 | }
1073 |
1074 | return (self::TYPE_ACCESS_ACCEPT == ($this->radiusPacketReceived));
1075 | }
1076 |
1077 | /**
1078 | * Perform an accessRequest against a list of servers. Each server must
1079 | * share the same RADIUS secret. This is useful if you have more than one
1080 | * RADIUS server. This function tries each server until it receives an
1081 | * Access-Accept or Access-Reject response. That is, it will try more than
1082 | * one server in the event of a timeout or other failure.
1083 | *
1084 | * @see \Dapphp\Radius\Radius::accessRequest()
1085 | *
1086 | * @param array $serverList Array of servers to authenticate against
1087 | * @param string $username Username to authenticate as
1088 | * @param string $password Password to authenticate with using PAP
1089 | * @param int $timeout The timeout (in seconds) to wait for a response packet
1090 | * @param string $state The state of the request (default is Service-Type=1)
1091 | *
1092 | * @return boolean true if the server sent an Access-Accept packet, false otherwise
1093 | */
1094 | public function accessRequestList($serverList, $username = '', $password = '', $timeout = 0, $state = null)
1095 | {
1096 | $result = false;
1097 |
1098 | if (!is_array($serverList)) {
1099 | $this->errorCode = 127;
1100 | $this->errorMessage = sprintf(
1101 | 'server list passed to accessRequestList must be array; %s given', gettype($serverList)
1102 | );
1103 |
1104 | return false;
1105 | }
1106 |
1107 | $attributes = $this->getAttributesToSend(); // store base attributes
1108 |
1109 | foreach($serverList as $server) {
1110 | $this->setServer($server);
1111 |
1112 | $result = $this->accessRequest($username, $password, $timeout, $state);
1113 |
1114 | if ($result === true) {
1115 | break; // success
1116 | } elseif ($this->getErrorCode() === self::TYPE_ACCESS_REJECT) {
1117 | break; // access rejected
1118 | } else {
1119 | /* timeout or other possible transient error; try next host */
1120 | $this->attributesToSend = $attributes; // reset base attributes
1121 | }
1122 | }
1123 |
1124 | return $result;
1125 | }
1126 |
1127 | /**
1128 | * Authenticate using EAP-MS-CHAP v2. This is a 4-way authentication
1129 | * process that sends an Access-Request, receives an Access-Challenge,
1130 | * responds with an Access-Request, and finally sends an Access-Request with
1131 | * an EAP success packet if the last Access-Challenge was a success.
1132 | *
1133 | * Windows Server NPS: EAP Type: MS-CHAP v2
1134 | *
1135 | * @param string $username The username to authenticate as
1136 | * @param string $password The plain text password that will be hashed using MS-CHAPv2
1137 | * @return boolean true if negotiation resulted in an Access-Accept packet, false otherwise
1138 | */
1139 | public function accessRequestEapMsChapV2($username, $password)
1140 | {
1141 | /*
1142 | * RADIUS EAP MS-CHAP-V2 Process:
1143 | * > RADIUS ACCESS_REQUEST w/ EAP identity packet
1144 | * < ACCESS_CHALLENGE w/ MS-CHAP challenge encapsulated in EAP request
1145 | * CHAP packet contains auth_challenge value
1146 | * Calculate encrypted password based on challenge for response
1147 | * > ACCESS_REQUEST w/ MS-CHAP challenge response, peer_challenge &
1148 | * encrypted password encapsulated in an EAP response packet
1149 | * < ACCESS_CHALLENGE w/ MS-CHAP success or failure in EAP packet.
1150 | * > ACCESS_REQUEST w/ EAP success packet if challenge was accepted
1151 | *
1152 | */
1153 |
1154 | $attributes = $this->getAttributesToSend();
1155 |
1156 |
1157 | // compose and send identity packet as a start of authentication
1158 | $eapPacket = EAPPacket::identity($username);
1159 |
1160 | $this->clearDataToSend()
1161 | ->clearError()
1162 | ->setPacketType(self::TYPE_ACCESS_REQUEST);
1163 |
1164 | $this->attributesToSend = $attributes;
1165 | $this->setUsername($username)
1166 | ->removeAttribute(79)
1167 | ->setAttribute(79, $eapPacket)
1168 | ->setIncludeMessageAuthenticator();
1169 |
1170 | $this->accessRequest();
1171 |
1172 | if ($this->errorCode) {
1173 | return false;
1174 | }
1175 |
1176 | if ($this->radiusPacketReceived != self::TYPE_ACCESS_CHALLENGE) {
1177 | $this->errorCode = 102;
1178 | $this->errorMessage = 'Access-Request did not get Access-Challenge response';
1179 | return false;
1180 | }
1181 |
1182 | $state = $this->getReceivedAttribute(24);
1183 | $eap = $this->getReceivedAttribute(79);
1184 |
1185 | if ($eap == null) {
1186 | $this->errorCode = 102;
1187 | $this->errorMessage = 'EAP packet missing from Radius access challenge packet';
1188 | return false;
1189 | }
1190 |
1191 | $eap = EAPPacket::fromString($eap);
1192 |
1193 | // checking what type of EAP-Message we have
1194 | // if it is a PEAP proposal, we start an EAP fallback
1195 | if ($eap->type == EAPPacket::TYPE_PEAP_EAP) { // fallback if PEAP
1196 | $eapId = $eap->id;
1197 |
1198 | $eapPacket = EAPPacket::legacyNak(EAPPacket::TYPE_EAP_MS_AUTH, $eapId);
1199 |
1200 | $this->clearDataToSend()
1201 | ->setPacketType(self::TYPE_ACCESS_REQUEST);
1202 |
1203 | $this->attributesToSend = $attributes;
1204 | $this->setUsername($username)
1205 | ->setAttribute(79, $eapPacket)
1206 | ->setIncludeMessageAuthenticator();
1207 |
1208 | $resp = $this->accessRequest('', '', 0, $state);
1209 |
1210 | if (!$resp) {
1211 | return false;
1212 | }
1213 |
1214 | $eap = $this->getReceivedAttribute(79);
1215 |
1216 | if ($eap == null) {
1217 | $this->errorCode = 102;
1218 | $this->errorMessage = 'EAP packet missing from Radius EAP fallback';
1219 | return false;
1220 | }
1221 |
1222 | $eap = EAPPacket::fromString($eap);
1223 | } elseif ($eap->type == EAPPacket::TYPE_MD5_CHALLENGE) {
1224 | // EAP type MD5, PPP CHAP protocol w/ MD5
1225 | $this->removeAttribute(79)
1226 | ->setChapPassword($password);
1227 |
1228 | return $this->accessRequest($username);
1229 | }
1230 |
1231 | // since we have check that we are not in PEAP method, we should be in EAP
1232 | // so let's check this and return error if not
1233 | if ($eap->type != EAPPacket::TYPE_EAP_MS_AUTH) {
1234 | $this->errorCode = 102;
1235 | $this->errorMessage = 'EAP type is not EAP_MS_AUTH or MD5_CHALLENGE in access response';
1236 | return false;
1237 | }
1238 |
1239 | $chapPacket = MsChapV2Packet::fromString($eap->data);
1240 |
1241 | if (!$chapPacket || $chapPacket->opcode != MsChapV2Packet::OPCODE_CHALLENGE) {
1242 | $this->errorCode = 102;
1243 | $this->errorMessage = 'MS-CHAP-V2 access response packet missing challenge';
1244 | return false;
1245 | }
1246 |
1247 | $challenge = $chapPacket->challenge;
1248 | $chapId = $chapPacket->msChapId;
1249 |
1250 | $msChapV2 = new \Crypt_CHAP_MSv2;
1251 | $msChapV2->username = $username;
1252 | $msChapV2->password = $password;
1253 | $msChapV2->chapid = $chapId;
1254 | $msChapV2->authChallenge = $challenge;
1255 |
1256 | $chapPacket->opcode = MsChapV2Packet::OPCODE_RESPONSE;
1257 | $chapPacket->response = $msChapV2->challengeResponse();
1258 | $chapPacket->name = $username;
1259 | $chapPacket->challenge = $msChapV2->peerChallenge;
1260 |
1261 | $eapPacket = EAPPacket::mschapv2($chapPacket, $chapId);
1262 |
1263 | $this->clearDataToSend()
1264 | ->setPacketType(self::TYPE_ACCESS_REQUEST);
1265 | $this->attributesToSend = $attributes;
1266 | $this->setUsername($username)
1267 | ->setAttribute(79, $eapPacket)
1268 | ->setIncludeMessageAuthenticator();
1269 |
1270 | $this->accessRequest('', '', 0, $state);
1271 |
1272 | if ($this->errorCode) {
1273 | return false;
1274 | }
1275 |
1276 | $eap = $this->getReceivedAttribute(79);
1277 |
1278 | if ($eap == null) {
1279 | $this->errorCode = 102;
1280 | $this->errorMessage = 'EAP packet missing from MS-CHAP-V2 challenge response';
1281 | return false;
1282 | }
1283 |
1284 | $eap = EAPPacket::fromString($eap);
1285 |
1286 | if ($eap->type != EAPPacket::TYPE_EAP_MS_AUTH) {
1287 | $this->errorCode = 102;
1288 | $this->errorMessage = 'EAP type is not EAP_MS_AUTH in access response';
1289 | return false;
1290 | }
1291 |
1292 | $chapPacket = MsChapV2Packet::fromString($eap->data);
1293 |
1294 | if ($chapPacket->opcode != MsChapV2Packet::OPCODE_SUCCESS) {
1295 | $this->errorCode = 3;
1296 |
1297 | $err = (!empty($chapPacket->response)) ? $chapPacket->response : 'General authentication failure';
1298 |
1299 | $pattern = '/E=(\d{1,10}).*R=(\d).*C=([0-9A-Fa-f]{32}).*V=(\d{1,10})/';
1300 |
1301 | if (preg_match($pattern, $chapPacket->response, $err)) {
1302 | switch($err[1]) {
1303 | case '691':
1304 | $err = 'Authentication failure, username or password incorrect.';
1305 | break;
1306 |
1307 | case '646':
1308 | $err = 'Authentication failure, restricted logon hours.';
1309 | break;
1310 |
1311 | case '647':
1312 | $err = 'Account disabled';
1313 | break;
1314 |
1315 | case '648':
1316 | $err = 'Password expired';
1317 | break;
1318 |
1319 | case '649':
1320 | $err = 'No dial in permission';
1321 | break;
1322 |
1323 | case '709':
1324 | $err = 'Error changing password';
1325 | break;
1326 | }
1327 | }
1328 |
1329 | $this->errorMessage = $err;
1330 | return false;
1331 | }
1332 |
1333 | // got a success response - send success acknowledgement
1334 | $eapPacket = EAPPacket::eapSuccess($chapId + 1);
1335 | $state = $this->getReceivedAttribute(24);
1336 |
1337 | $this->clearDataToSend()
1338 | ->setPacketType(self::TYPE_ACCESS_REQUEST);
1339 | $this->attributesToSend = $attributes;
1340 | $this->setUsername($username)
1341 | ->setAttribute(79, $eapPacket)
1342 | ->setIncludeMessageAuthenticator();
1343 |
1344 | return $this->accessRequest('', '', 0, $state);
1345 | }
1346 |
1347 | /**
1348 | * Allows the peer to change the password on the account specified in the preceding Response packet. The Change-Password
1349 | * packet should be sent only if the authenticator reports ERROR_PASSWD_EXPIRED (E=648) in the Message field of the
1350 | * Failure packet. RFC 2759 - 7. Change-Password Packet
1351 | *
1352 | * @param string $username The account username
1353 | * @param string $password The expired password
1354 | * @param string $newPassword The new password for the account
1355 | * @return bool true if the password was changed, otherwise false and $this->errorCode and $this->errorMessage are set
1356 | */
1357 | public function changePasswordEapMsChapV2($username, $password, $newPassword)
1358 | {
1359 | $this->removeAttribute(79);
1360 | $attributes = $this->getAttributesToSend();
1361 |
1362 | /*
1363 | $resp may be:
1364 | true in case of valid auth (not expired, not disabled, good pwd...)
1365 | false with chap-opcode=failure and err=648
1366 | false with other cases
1367 | */
1368 | $resp = $this->accessRequestEapMsChapV2($username, $password);
1369 |
1370 | if ($resp) {
1371 | $this->errorCode = 3;
1372 | $this->errorMessage = 'Password must be expired to be changed';
1373 | return false;
1374 | }
1375 |
1376 | if ($this->radiusPacketReceived == self::TYPE_ACCESS_REJECT) {
1377 | $this->errorCode = 3;
1378 | $this->errorMessage = 'Access rejected, invalid account';
1379 | return false;
1380 | } elseif ($this->radiusPacketReceived != self::TYPE_ACCESS_CHALLENGE) {
1381 | $this->errorCode = 102;
1382 | $this->errorMessage = 'Access-Request did not get Access-Challenge response';
1383 | return false;
1384 | }
1385 |
1386 | $state = $this->getReceivedAttribute(24);
1387 | $eap = $this->getReceivedAttribute(79);
1388 |
1389 | if ($eap == null) {
1390 | $this->errorCode = 102;
1391 | $this->errorMessage = 'EAP packet missing from Radius access challenge packet';
1392 | return false;
1393 | }
1394 |
1395 | $eap = EAPPacket::fromString($eap);
1396 |
1397 | if ($eap->type != EAPPacket::TYPE_EAP_MS_AUTH) {
1398 | $this->errorCode = 102;
1399 | $this->errorMessage = 'EAP type is not EAP_MS_AUTH in access response';
1400 | return false;
1401 | }
1402 |
1403 | $chapPacket = MsChapV2Packet::fromString($eap->data);
1404 |
1405 | // chap response opcode should be OPCODE_FAILURE, other cases are exceptions
1406 | if (!$chapPacket || $chapPacket->opcode != MsChapV2Packet::OPCODE_FAILURE) {
1407 | $this->errorCode = 102;
1408 | $this->errorMessage = 'Invalid reply from auth server';
1409 | return false;
1410 | }
1411 |
1412 | $err = (!empty($chapPacket->response)) ? $chapPacket->response : 'General authentication failure';
1413 | $pattern = '/E=(\d{1,10}).*R=(\d).*C=([0-9A-Fa-f]{32}).*V=(\d{1,10})/';
1414 | $pm = preg_match($pattern, $chapPacket->response, $err);
1415 |
1416 | if (!$pm) {
1417 | $this->errorCode = 102;
1418 | $this->errorMessage = 'Invalid reply from auth server';
1419 | return false;
1420 | }
1421 |
1422 | if ($err[1] == '648') {
1423 | $challenge = pack("H*", $err[3]);
1424 | } else {
1425 | switch($err[1]) {
1426 | case '691':
1427 | $err = 'Authentication failure, username or password incorrect.';
1428 | break;
1429 |
1430 | case '646':
1431 | $err = 'Authentication failure, restricted logon hours.';
1432 | break;
1433 |
1434 | case '647':
1435 | $err = 'Account disabled';
1436 | break;
1437 |
1438 | case '649':
1439 | $err = 'No dial in permission';
1440 | break;
1441 |
1442 | case '709':
1443 | $err = 'Error changing password';
1444 | break;
1445 | }
1446 |
1447 | $this->errorCode = 3;
1448 | $this->errorMessage = $err;
1449 | return false;
1450 | }
1451 |
1452 | $chapId = $chapPacket->msChapId + 1;
1453 |
1454 | $msChapV2 = new \Crypt_CHAP_MSv2;
1455 | $msChapV2->username = $username;
1456 | $msChapV2->password = $password;
1457 | $msChapV2->chapid = $chapId;
1458 | $msChapV2->authChallenge = $challenge;
1459 |
1460 | $chapPacket->opcode = MsChapV2Packet::OPCODE_CHANGEPASS;
1461 | $chapPacket->msChapId = $chapId;
1462 | $chapPacket->name = $username;
1463 | $chapPacket->response = $msChapV2->challengeResponse();
1464 | $chapPacket->challenge = $msChapV2->peerChallenge;
1465 | $chapPacket->encryptedPwd = $msChapV2->newPasswordEncryptedWithOldNtPasswordHash($newPassword, $password);
1466 | $chapPacket->encryptedHash = $msChapV2->oldNtPasswordHashEncryptedWithNewNtPasswordHash($newPassword, $password);
1467 |
1468 | $eapPacketSplit = str_split(EAPPacket::mschapv2($chapPacket, $chapId), 253);
1469 |
1470 | $this->clearDataToSend()
1471 | ->setPacketType(self::TYPE_ACCESS_REQUEST);
1472 | $this->attributesToSend = $attributes;
1473 | $this->setUsername($username)
1474 | ->setAttribute(79, $eapPacketSplit[0])
1475 | ->setAttribute(79, $eapPacketSplit[1])
1476 | ->setAttribute(79, $eapPacketSplit[2])
1477 | ->setIncludeMessageAuthenticator();
1478 |
1479 | $resp = $this->accessRequest('', '', 0, $state);
1480 |
1481 | if ($this->errorCode) {
1482 | $this->errorMessage = 'Password change rejected; new password may not meet the password policy requirements';
1483 | return false;
1484 | }
1485 |
1486 | // got a success response - send success acknowledgement
1487 | $eapPacket = EAPPacket::eapSuccess($chapId + 1);
1488 |
1489 | $this->clearDataToSend()
1490 | ->setPacketType(self::TYPE_ACCESS_REQUEST);
1491 | $this->attributesToSend = $attributes;
1492 | $this->setUsername($username)
1493 | ->setAttribute(79, $eapPacket)
1494 | ->setIncludeMessageAuthenticator();
1495 |
1496 | // returns true if password changed successfully
1497 | return $this->accessRequest('', '', 0, $state);
1498 | }
1499 |
1500 | /**
1501 | * Perform a EAP-MS-CHAP v2 4-way authentication against a list of servers.
1502 | * Each server must share the same RADIUS secret.
1503 | *
1504 | * @see \Dapphp\Radius\Radius::accessRequestEapMsChapV2()
1505 | * @see \Dapphp\Radius\Radius::accessRequestList()
1506 | *
1507 | * @param array $serverList Array of servers to authenticate against
1508 | * @param string $username The username to authenticate as
1509 | * @param string $password The plain text password that will be hashed using MS-CHAPv2
1510 | * @return boolean true if negotiation resulted in an Access-Accept packet, false otherwise
1511 | */
1512 | public function accessRequestEapMsChapV2List($serverList, $username, $password)
1513 | {
1514 | $result = false;
1515 |
1516 | if (!is_array($serverList)) {
1517 | $this->errorCode = 127;
1518 | $this->errorMessage = sprintf(
1519 | 'server list passed to accessRequestEapMsChapV2List must be array; %s given', gettype($serverList)
1520 | );
1521 |
1522 | return false;
1523 | }
1524 |
1525 | $attributes = $this->getAttributesToSend(); // store base attributes
1526 |
1527 | foreach($serverList as $server) {
1528 | $this->setServer($server);
1529 |
1530 | $result = $this->accessRequestEapMsChapV2($username, $password);
1531 |
1532 | if ($result === true) {
1533 | break; // success
1534 | } elseif ($this->getErrorCode() === self::TYPE_ACCESS_REJECT) {
1535 | break; // access rejected
1536 | } else {
1537 | /* timeout or other possible transient error; try next host */
1538 | $this->attributesToSend = $attributes; // reset base attributes
1539 | }
1540 | }
1541 |
1542 | return $result;
1543 | }
1544 |
1545 | /**
1546 | * Send a RADIUS packet over the wire using UDP.
1547 | *
1548 | * @param string $packetData The raw, complete, RADIUS packet to send
1549 | * @return boolean|resource false if the packet failed to send, or a socket resource on success
1550 | */
1551 | private function sendRadiusRequest($packetData)
1552 | {
1553 | $packetLen = strlen($packetData);
1554 |
1555 | $conn = @fsockopen('udp://' . $this->server, $this->authenticationPort, $errno, $errstr);
1556 | if (!$conn) {
1557 | $this->errorCode = $errno;
1558 | $this->errorMessage = $errstr;
1559 | return false;
1560 | }
1561 |
1562 | $sent = fwrite($conn, $packetData);
1563 | if (!$sent || $packetLen != $sent) {
1564 | $this->errorCode = 55; // CURLE_SEND_ERROR
1565 | $this->errorMessage = 'Failed to send UDP packet';
1566 | return false;
1567 | }
1568 |
1569 | if ($this->debug) {
1570 | $this->debugInfo(
1571 | sprintf(
1572 | 'Packet type %d (%s) sent to %s',
1573 | $this->radiusPacket,
1574 | $this->getRadiusPacketInfo($this->radiusPacket),
1575 | $this->server
1576 | )
1577 | );
1578 | foreach($this->attributesToSend as $attrs) {
1579 | if (!is_array($attrs)) {
1580 | $attrs = array($attrs);
1581 | }
1582 |
1583 | foreach($attrs as $attr) {
1584 | $attrInfo = $this->getAttributesInfo(ord(substr($attr, 0, 1)));
1585 | $this->debugInfo(
1586 | sprintf(
1587 | 'Attribute %d (%s), length (%d), format %s, value %s',
1588 | ord(substr($attr, 0, 1)),
1589 | $attrInfo[0],
1590 | ord(substr($attr, 1, 1)) - 2,
1591 | $attrInfo[1],
1592 | $this->decodeAttribute(substr($attr, 2), ord(substr($attr, 0, 1)))
1593 | )
1594 | );
1595 | }
1596 | }
1597 | }
1598 |
1599 | return $conn;
1600 | }
1601 |
1602 | /**
1603 | * Wait for a UDP response packet and read using a timeout.
1604 | *
1605 | * @param resource $conn The connection resource returned by fsockopen
1606 | * @return boolean|string false on failure, or the RADIUS response packet
1607 | */
1608 | private function readRadiusResponse($conn)
1609 | {
1610 | stream_set_blocking($conn, false);
1611 | $read = array($conn);
1612 | $write = null;
1613 | $except = null;
1614 |
1615 | $receivedPacket = '';
1616 | $packetLen = null;
1617 | $elapsed = 0;
1618 |
1619 | do {
1620 | // Loop until the entire packet is read. Even with small packets,
1621 | // not all data might get returned in one read on a non-blocking stream.
1622 |
1623 | $t0 = microtime(true);
1624 | $changed = stream_select($read, $write, $except, $this->timeout);
1625 | $t1 = microtime(true);
1626 |
1627 | if ($changed > 0) {
1628 | $data = fgets($conn, 1024);
1629 | // Try to read as much data from the stream in one pass until 4
1630 | // bytes are read. Once we have 4 bytes, we can determine the
1631 | // length of the RADIUS response to know when to stop reading.
1632 |
1633 | if ($data === false) {
1634 | // recv could fail due to ICMP destination unreachable
1635 | $this->errorCode = 56; // CURLE_RECV_ERROR
1636 | $this->errorMessage = 'Failure with receiving network data';
1637 | return false;
1638 | }
1639 |
1640 | $receivedPacket .= $data;
1641 |
1642 | if (strlen($receivedPacket) < 4) {
1643 | // not enough data to get the size
1644 | // this will probably never happen
1645 | continue;
1646 | }
1647 |
1648 | if ($packetLen == null) {
1649 | // first pass - decode the packet size from response
1650 | $packetLen = unpack('n', substr($receivedPacket, 2, 2));
1651 | $packetLen = (int)array_shift($packetLen);
1652 |
1653 | if ($packetLen < 4 || $packetLen > 65507) {
1654 | $this->errorCode = 102;
1655 | $this->errorMessage = "Bad packet size in RADIUS response. Got {$packetLen}";
1656 | return false;
1657 | }
1658 | }
1659 |
1660 | } elseif ($changed === false) {
1661 | $this->errorCode = 2;
1662 | $this->errorMessage = 'stream_select returned false';
1663 | return false;
1664 | } else {
1665 | $this->errorCode = 28; // CURLE_OPERATION_TIMEDOUT
1666 | $this->errorMessage = 'Timed out while waiting for RADIUS response';
1667 | return false;
1668 | }
1669 |
1670 | $elapsed += ($t1 - $t0);
1671 | } while ($elapsed < $this->timeout && strlen($receivedPacket) < $packetLen);
1672 |
1673 | return $receivedPacket;
1674 | }
1675 |
1676 | /**
1677 | * Parse a response packet and do some basic validation.
1678 | *
1679 | * @param string $packet The raw RADIUS response packet
1680 | * @return boolean true if the packet was decoded, false otherwise.
1681 | */
1682 | private function parseRadiusResponsePacket($packet)
1683 | {
1684 | $this->radiusPacketReceived = intval(ord(substr($packet, 0, 1)));
1685 |
1686 | $this->debugInfo(sprintf(
1687 | 'Packet type %d (%s) received',
1688 | $this->radiusPacketReceived,
1689 | $this->getRadiusPacketInfo($this->getResponsePacket())
1690 | ));
1691 |
1692 | if ($this->radiusPacketReceived > 0) {
1693 | $this->identifierReceived = intval(ord(substr($packet, 1, 1)));
1694 | $packetLenRx = unpack('n', substr($packet, 2, 2));
1695 | $packetLenRx = array_shift($packetLenRx);
1696 | $this->responseAuthenticator = bin2hex(substr($packet, 4, 16));
1697 | if ($packetLenRx > 20) {
1698 | $attrContent = substr($packet, 20);
1699 | } else {
1700 | $attrContent = '';
1701 | }
1702 |
1703 | $authCheck = md5(
1704 | substr($packet, 0, 4) .
1705 | $this->getRequestAuthenticator() .
1706 | $attrContent .
1707 | $this->getSecret()
1708 | );
1709 |
1710 | if ($authCheck !== $this->responseAuthenticator) {
1711 | $this->errorCode = 101;
1712 | $this->errorMessage = 'Response authenticator in received packet did not match expected value';
1713 | return false;
1714 | }
1715 |
1716 | while (strlen($attrContent) > 2) {
1717 | $attrType = intval(ord(substr($attrContent, 0, 1)));
1718 | $attrLength = intval(ord(substr($attrContent, 1, 1)));
1719 | $attrValueRaw = substr($attrContent, 2, $attrLength - 2);
1720 | $attrContent = substr($attrContent, $attrLength);
1721 | $attrValue = $this->decodeAttribute($attrValueRaw, $attrType);
1722 |
1723 | $attr = $this->getAttributesInfo($attrType);
1724 | if (26 == $attrType) {
1725 | $vendorArr = $this->decodeVendorSpecificContent($attrValue);
1726 | foreach($vendorArr as $vendor) {
1727 | $this->debugInfo(
1728 | sprintf(
1729 | 'Attribute %d (%s), length %d, format %s, Vendor-Id: %d, Vendor-type: %s, Attribute-specific: %s',
1730 | $attrType, $attr[0], $attrLength - 2,
1731 | $attr[1], $vendor[0], $vendor[1], $vendor[2]
1732 | )
1733 | );
1734 | }
1735 | } else {
1736 | $this->debugInfo(
1737 | sprintf(
1738 | 'Attribute %d (%s), length %d, format %s, value %s',
1739 | $attrType, $attr[0], $attrLength - 2, $attr[1], $attrValue
1740 | )
1741 | );
1742 | }
1743 |
1744 | // TODO: check message authenticator
1745 |
1746 | $this->attributesReceived[] = array($attrType, $attrValue);
1747 | }
1748 | } else {
1749 | $this->errorCode = 100;
1750 | $this->errorMessage = 'Invalid response packet received';
1751 | return false;
1752 | }
1753 |
1754 | return true;
1755 | }
1756 |
1757 | /**
1758 | * Generate a RADIUS packet based on the set attributes and properties.
1759 | * Generally, there is no need to call this function. Use one of the accessRequest* functions.
1760 | *
1761 | * @return string The RADIUS packet
1762 | */
1763 | public function generateRadiusPacket()
1764 | {
1765 | $hasAuthenticator = false;
1766 | $attrContent = '';
1767 | $offset = null;
1768 |
1769 | foreach($this->attributesToSend as $i => $attr) {
1770 | $len = strlen($attrContent);
1771 |
1772 | if (is_array($attr)) {
1773 | // vendor specific (could have multiple attributes)
1774 | $attrContent .= implode('', $attr);
1775 | } else {
1776 | if (ord($attr[0]) == 80) {
1777 | // If Message-Authenticator is set, note offset so it can be updated
1778 | $hasAuthenticator = true;
1779 | $offset = $len + 2; // current length + type(1) + length(1)
1780 | }
1781 |
1782 | $attrContent .= $attr;
1783 | }
1784 | }
1785 |
1786 | $attrLen = strlen($attrContent);
1787 | $packetLen = 4; // Radius packet code + Identifier + Length high + Length low
1788 | $packetLen += strlen($this->getRequestAuthenticator()); // Request-Authenticator
1789 | $packetLen += $attrLen; // Attributes
1790 |
1791 | $packetData = chr($this->radiusPacket);
1792 | $packetData .= pack('C', $this->getNextIdentifier());
1793 | $packetData .= pack('n', $packetLen);
1794 | $packetData .= $this->getRequestAuthenticator();
1795 | $packetData .= $attrContent;
1796 |
1797 | if ($hasAuthenticator && !is_null($offset)) {
1798 | $messageAuthenticator = hash_hmac('md5', $packetData, $this->secret, true);
1799 | // calculate packet hmac, replace hex 0's with actual hash
1800 | for ($i = 0; $i < strlen($messageAuthenticator); ++$i) {
1801 | $packetData[20 + $offset + $i] = $messageAuthenticator[$i];
1802 | }
1803 | }
1804 |
1805 | return $packetData;
1806 | }
1807 |
1808 | /**
1809 | * Set the RADIUS packet identifier that will be used for the next request
1810 | *
1811 | * @param int $identifierToSend The packet identifier to send
1812 | * @return self
1813 | */
1814 | public function setNextIdentifier($identifierToSend = 0)
1815 | {
1816 | $id = (int)$identifierToSend;
1817 |
1818 | $this->identifierToSend = $id - 1;
1819 |
1820 | return $this;
1821 | }
1822 |
1823 | /**
1824 | * Increment the packet identifier and return the number number
1825 | *
1826 | * @return int The radius packet id
1827 | */
1828 | public function getNextIdentifier()
1829 | {
1830 | $this->identifierToSend = (($this->identifierToSend + 1) % 256);
1831 | return $this->identifierToSend;
1832 | }
1833 |
1834 | private function generateRequestAuthenticator()
1835 | {
1836 | $this->requestAuthenticator = '';
1837 |
1838 | for ($c = 0; $c <= 15; ++$c) {
1839 | $this->requestAuthenticator .= chr(rand(1, 255));
1840 | }
1841 |
1842 | return $this;
1843 | }
1844 |
1845 | /**
1846 | * Set the request authenticator for the packet. This is for testing only.
1847 | * There is no need to ever call this function.
1848 | *
1849 | * @param string $requestAuthenticator The 16 octet request identifier
1850 | * @return boolean|self false if the authenticator is invalid length, self otherwise
1851 | */
1852 | public function setRequestAuthenticator($requestAuthenticator)
1853 | {
1854 | if (strlen($requestAuthenticator) != 16) {
1855 | return false;
1856 | }
1857 |
1858 | $this->requestAuthenticator = $requestAuthenticator;
1859 |
1860 | return $this;
1861 | }
1862 |
1863 | /**
1864 | * Get the value of the request authenticator used in request packets
1865 | *
1866 | * @return string 16 octet request authenticator
1867 | */
1868 | public function getRequestAuthenticator()
1869 | {
1870 | return $this->requestAuthenticator;
1871 | }
1872 |
1873 | protected function clearDataToSend()
1874 | {
1875 | $this->radiusPacket = 0;
1876 | $this->attributesToSend = null;
1877 | return $this;
1878 | }
1879 |
1880 | protected function clearDataReceived()
1881 | {
1882 | $this->radiusPacketReceived = 0;
1883 | $this->attributesReceived = null;
1884 | return $this;
1885 | }
1886 |
1887 | public function setPacketType($type)
1888 | {
1889 | $this->radiusPacket = $type;
1890 | return $this;
1891 | }
1892 |
1893 | private function clearError()
1894 | {
1895 | $this->errorCode = 0;
1896 | $this->errorMessage = '';
1897 |
1898 | return $this;
1899 | }
1900 |
1901 | protected function debugInfo($message)
1902 | {
1903 | if ($this->debug) {
1904 | $msg = date('Y-m-d H:i:s'). ' DEBUG: ';
1905 | $msg .= $message;
1906 | $msg .= "
\n";
1907 |
1908 | if (php_sapi_name() == 'cli') {
1909 | $msg = strip_tags($msg);
1910 | }
1911 |
1912 | echo $msg;
1913 | flush();
1914 | }
1915 | }
1916 |
1917 | private function decodeAttribute($rawValue, $attributeFormat)
1918 | {
1919 | $value = null;
1920 |
1921 | if (isset($this->attributesInfo[$attributeFormat])) {
1922 | switch ($this->attributesInfo[$attributeFormat][1]) {
1923 | case 'T':
1924 | case 'S':
1925 | $value = $rawValue;
1926 | break;
1927 |
1928 | case 'A':
1929 | $value = ord(substr($rawValue, 0, 1)) . '.' .
1930 | ord(substr($rawValue, 1, 1)) . '.' .
1931 | ord(substr($rawValue, 2, 1)) . '.' .
1932 | ord(substr($rawValue, 3, 1));
1933 | break;
1934 |
1935 | case 'I':
1936 | $value = (ord(substr($rawValue, 0, 1)) * 256 * 256 * 256) +
1937 | (ord(substr($rawValue, 1, 1)) * 256 * 256) +
1938 | (ord(substr($rawValue, 2, 1)) * 256) +
1939 | ord(substr($rawValue, 3, 1));
1940 | break;
1941 |
1942 | case 'D':
1943 | $value = null;
1944 | break;
1945 |
1946 | default:
1947 | $value = null;
1948 | }
1949 | }
1950 |
1951 | return $value;
1952 | }
1953 | }
1954 |
--------------------------------------------------------------------------------