├── 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 | Build Status 3 | Total Downloads 4 | Latest Stable Version 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 | --------------------------------------------------------------------------------