Files

1318 lines
42 KiB
PHP

<?php
/*********************************************************************
*
* Pure PHP radius class
*
* This Radius class is a radius client implementation in pure PHP
* following the RFC 2865 rules (http://www.ietf.org/rfc/rfc2865.txt)
*
* This class works with at least the following RADIUS servers:
* - Authenex Strong Authentication System (ASAS) with two-factor authentication
* - FreeRADIUS, a free Radius server implementation for Linux and *nix environments
* - Microsoft Radius server IAS
* - Microsoft Windows Server 2012 R2 (Network Policy Server)
* - Mideye RADIUS server (http://www.mideye.com)
* - Radl, a free Radius server for Windows
* - RSA SecurID
* - VASCO Middleware 3.0 server
* - WinRadius, Windows Radius server (free for 5 users)
* - ZyXEL ZyWALL OTP (Authenex ASAS branded by ZyXEL, cheaper)
*
*
* LICENCE
*
* Copyright (c) 2008, SysCo systemes de communication sa
* SysCo (tm) is a trademark of SysCo systemes de communication sa
* (http://www.sysco.ch/)
* All rights reserved.
*
* Copyright (c) 2016, Drew Phillips
* (https://drew-phillips.com)
*
* This file is part of the Pure PHP radius class
*
* Pure PHP radius class is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* Pure PHP radius class is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with Pure PHP radius class.
* If not, see <http://www.gnu.org/licenses/>.
*
*
* @author: SysCo/al
* @author: Drew Phillips <drew@drew-phillips.com>
* @since CreationDate: 2008-01-04
* @copyright (c) 2008 by SysCo systemes de communication sa
* @copyright (c) 2016 by Drew Phillips
* @version 2.5.0
* @link http://developer.sysco.ch/php/
* @link developer@sysco.ch
* @link https://github.com/dapphp/radius
* @link drew@drew-phillips.com
*/
namespace Dapphp\Radius;
/**
* A pure PHP RADIUS client implementation.
*
* Originally created by SysCo/al based on radius.class.php v1.2.2
* Modified for PHP5 & PHP7 compatibility by Drew Phillips
* Switched from using ext/sockets to streams.
*
*/
class Radius
{
/** @var int Access-Request packet type identifier */
const TYPE_ACCESS_REQUEST = 1;
/** @var int Access-Accept packet type identifier */
const TYPE_ACCESS_ACCEPT = 2;
/** @var int Access-Reject packet type identifier */
const TYPE_ACCESS_REJECT = 3;
/** @var int Accounting-Request packet type identifier */
const TYPE_ACCOUNTING_REQUEST = 4;
/** @var int Accounting-Response packet type identifier */
const TYPE_ACCOUNTING_RESPONSE = 5;
/** @var int Access-Challenge packet type identifier */
const TYPE_ACCESS_CHALLENGE = 11;
/** @var int Reserved packet type */
const TYPE_RESERVED = 255;
/** @var string RADIUS server hostname or IP address */
protected $server;
/** @var string Shared secret with the RADIUS server */
protected $secret;
/** @var string RADIUS suffix (default is '') */
protected $suffix;
/** @var int Timeout for receiving UDP response packets (default = 5 seconds) */
protected $timeout;
/** @var int Authentication port (default = 1812) */
protected $authenticationPort;
/** @var int Accounting port (default = 1813) */
protected $accountingPort;
/** @var string Network Access Server (client) IP Address */
protected $nasIpAddress;
/** @var string NAS port. Physical port of the NAS authenticating the user */
protected $nasPort;
/** @var string Encrypted password, as described in RFC 2865 */
protected $encryptedPassword;
/** @var int Request-Authenticator, 16 octets random number */
protected $requestAuthenticator;
/** @var int Request-Authenticator from the response */
protected $responseAuthenticator;
/** @var string Username to send to the RADIUS server */
protected $username;
/** @var string Password for authenticating with the RADIUS server (before encryption) */
protected $password;
/** @var int The CHAP identifier for CHAP-Password attributes */
protected $chapIdentifier;
/** @var string Identifier field for the packet to be sent */
protected $identifierToSend;
/** @var string Identifier field for the received packet */
protected $identifierReceived;
/** @var int RADIUS packet type (1=Access-Request, 2=Access-Accept, etc) */
protected $radiusPacket;
/** @var int Packet type received in response from RADIUS server */
protected $radiusPacketReceived;
/** @var array List of RADIUS attributes to send */
protected $attributesToSend;
/** @var array List of attributes received in response */
protected $attributesReceived;
/** @var bool Whether or not to enable debug output */
protected $debug;
/** @var array RADIUS attributes info array */
protected $attributesInfo;
/** @var array RADIUS packet codes info array */
protected $radiusPackets;
/** @var int The error code from the last operation */
protected $errorCode;
/** @var string The error message from the last operation */
protected $errorMessage;
public function __construct($radiusHost = '127.0.0.1',
$sharedSecret = '',
$radiusSuffix = '',
$timeout = 5,
$authenticationPort = 1812,
$accountingPort = 1813)
{
$this->radiusPackets = array();
$this->radiusPackets[1] = 'Access-Request';
$this->radiusPackets[2] = 'Access-Accept';
$this->radiusPackets[3] = 'Access-Reject';
$this->radiusPackets[4] = 'Accounting-Request';
$this->radiusPackets[5] = 'Accounting-Response';
$this->radiusPackets[11] = 'Access-Challenge';
$this->radiusPackets[12] = 'Status-Server (experimental)';
$this->radiusPackets[13] = 'Status-Client (experimental)';
$this->radiusPackets[255] = 'Reserved';
$this->attributesInfo = array();
$this->attributesInfo[1] = array('User-Name', 'S');
$this->attributesInfo[2] = array('User-Password', 'S');
$this->attributesInfo[3] = array('CHAP-Password', 'S'); // Type (1) / Length (1) / CHAP Ident (1) / String
$this->attributesInfo[4] = array('NAS-IP-Address', 'A');
$this->attributesInfo[5] = array('NAS-Port', 'I');
$this->attributesInfo[6] = array('Service-Type', 'I');
$this->attributesInfo[7] = array('Framed-Protocol', 'I');
$this->attributesInfo[8] = array('Framed-IP-Address', 'A');
$this->attributesInfo[9] = array('Framed-IP-Netmask', 'A');
$this->attributesInfo[10] = array('Framed-Routing', 'I');
$this->attributesInfo[11] = array('Filter-Id', 'T');
$this->attributesInfo[12] = array('Framed-MTU', 'I');
$this->attributesInfo[13] = array('Framed-Compression', 'I');
$this->attributesInfo[14] = array('Login-IP-Host', 'A');
$this->attributesInfo[15] = array('Login-service', 'I');
$this->attributesInfo[16] = array('Login-TCP-Port', 'I');
$this->attributesInfo[17] = array('(unassigned)', '');
$this->attributesInfo[18] = array('Reply-Message', 'T');
$this->attributesInfo[19] = array('Callback-Number', 'S');
$this->attributesInfo[20] = array('Callback-Id', 'S');
$this->attributesInfo[21] = array('(unassigned)', '');
$this->attributesInfo[22] = array('Framed-Route', 'T');
$this->attributesInfo[23] = array('Framed-IPX-Network', 'I');
$this->attributesInfo[24] = array('State', 'S');
$this->attributesInfo[25] = array('Class', 'S');
$this->attributesInfo[26] = array('Vendor-Specific', 'S'); // Type (1) / Length (1) / Vendor-Id (4) / Vendor type (1) / Vendor length (1) / Attribute-Specific...
$this->attributesInfo[27] = array('Session-Timeout', 'I');
$this->attributesInfo[28] = array('Idle-Timeout', 'I');
$this->attributesInfo[29] = array('Termination-Action', 'I');
$this->attributesInfo[30] = array('Called-Station-Id', 'S');
$this->attributesInfo[31] = array('Calling-Station-Id', 'S');
$this->attributesInfo[32] = array('NAS-Identifier', 'S');
$this->attributesInfo[33] = array('Proxy-State', 'S');
$this->attributesInfo[34] = array('Login-LAT-Service', 'S');
$this->attributesInfo[35] = array('Login-LAT-Node', 'S');
$this->attributesInfo[36] = array('Login-LAT-Group', 'S');
$this->attributesInfo[37] = array('Framed-AppleTalk-Link', 'I');
$this->attributesInfo[38] = array('Framed-AppleTalk-Network', 'I');
$this->attributesInfo[39] = array('Framed-AppleTalk-Zone', 'S');
$this->attributesInfo[60] = array('CHAP-Challenge', 'S');
$this->attributesInfo[61] = array('NAS-Port-Type', 'I');
$this->attributesInfo[62] = array('Port-Limit', 'I');
$this->attributesInfo[63] = array('Login-LAT-Port', 'S');
$this->attributesInfo[76] = array('Prompt', 'I');
$this->attributesInfo[79] = array('EAP-Message', 'S');
$this->attributesInfo[80] = array('Message-Authenticator', 'S');
$this->identifierToSend = -1;
$this->chapIdentifier = 1;
$this->generateRequestAuthenticator()
->setServer($radiusHost)
->setSecret($sharedSecret)
->setAuthenticationPort($authenticationPort)
->setAccountingPort($accountingPort)
->setTimeout($timeout)
->setRadiusSuffix($radiusSuffix);
$this->clearError()
->clearDataToSend()
->clearDataReceived();
}
public function getLastError()
{
if (0 < $this->errorCode) {
return $this->errorMessage.' ('.$this->errorCode.')';
} else {
return '';
}
}
public function getErrorCode()
{
return $this->errorCode;
}
public function getErrorMessage()
{
return $this->errorMessage;
}
public function setDebug($enabled = true)
{
$this->debug = (true === $enabled);
return $this;
}
public function setServer($hostOrIp)
{
$this->server = gethostbyname($hostOrIp);
return $this;
}
public function setSecret($secret)
{
$this->secret = $secret;
return $this;
}
public function getSecret()
{
return $this->secret;
}
public function setRadiusSuffix($suffix)
{
$this->suffix = $suffix;
return $this;
}
public function setUsername($username = '')
{
if (false === strpos($username, '@'))
{
$username .= $this->suffix;
}
$this->username = $username;
$this->setAttribute(1, $this->username);
return $this;
}
public function getUsername()
{
return $this->username;
}
public function setPassword($password)
{
$this->password = $password;
$encryptedPassword = $this->getEncryptedPassword($password, $this->getSecret(), $this->getRequestAuthenticator());
$this->setAttribute(2, $encryptedPassword);
return $this;
}
public function getPassword()
{
return $this->password;
}
public function getEncryptedPassword($password, $secret, $requestAuthenticator)
{
$encryptedPassword = '';
$paddedPassword = $password;
if (0 != (strlen($password) % 16)) {
$paddedPassword .= str_repeat(chr(0), (16 - strlen($password) % 16));
}
$previous = $requestAuthenticator;
for ($i = 0; $i < (strlen($paddedPassword) / 16); ++$i) {
$temp = md5($secret . $previous);
$previous = '';
for ($j = 0; $j <= 15; ++$j) {
$value1 = ord(substr($paddedPassword, ($i * 16) + $j, 1));
$value2 = hexdec(substr($temp, 2 * $j, 2));
$xor_result = $value1 ^ $value2;
$previous .= chr($xor_result);
}
$encryptedPassword .= $previous;
}
return $encryptedPassword;
}
public function setIncludeMessageAuthenticator($include = true)
{
if ($include) {
$this->setAttribute(80, str_repeat("\x00", 16));
} else {
$this->removeAttribute(80);
}
return $this;
}
public function setChapId($nextId)
{
$this->chapIdentifier = (int)$nextId;
return $this;
}
public function getChapId()
{
$id = $this->chapIdentifier;
$this->chapIdentifier++;
return $id;
}
public function setChapPassword($password)
{
$chapId = $this->getChapId();
$chapMd5 = $this->getChapPassword($password, $chapId, $this->getRequestAuthenticator());
$this->setAttribute(3, pack('C', $chapId) . $chapMd5);
return $this;
}
public function getChapPassword($password, $chapId, $requestAuthenticator)
{
return md5(pack('C', $chapId) . $password . $requestAuthenticator, true);
}
public function setMsChapPassword($password, $challenge = null)
{
$chap = new \Crypt_CHAP_MSv1();
$chap->chapid = mt_rand(1, 255);
$chap->password = $password;
if (is_null($challenge)) {
$chap->generateChallenge();
} else {
$chap->challenge = $challenge;
}
$response = "\x00\x01" . str_repeat ("\0", 24) . $chap->ntChallengeResponse();
$this->setIncludeMessageAuthenticator();
$this->setVendorSpecificAttribute(VendorId::MICROSOFT, 11, $chap->challenge);
$this->setVendorSpecificAttribute(VendorId::MICROSOFT, 1, $response);
return $this;
}
public function setNasIPAddress($hostOrIp = '')
{
if (0 < strlen($hostOrIp)) {
$this->nasIpAddress = gethostbyname($hostOrIp);
} else {
$hostOrIp = @php_uname('n');
if (empty($hostOrIp)) {
$hostOrIp = (isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : '';
}
if (empty($hostOrIp)) {
$hostOrIp = (isset($_SERVER['SERVER_ADDR'])) ? $_SERVER['SERVER_ADDR'] : '0.0.0.0';
}
$this->nasIpAddress = gethostbyname($hostOrIp);
}
$this->setAttribute(4, $this->nasIpAddress);
return $this;
}
public function getNasIPAddress()
{
return $this->nasIpAddress;
}
public function setNasPort($port = 0)
{
$this->nasPort = intval($port);
$this->setAttribute(5, $this->nasPort);
return $this;
}
public function getNasPort()
{
return $this->nasPort;
}
public function setTimeout($timeout = 5)
{
if (intval($timeout) > 0) {
$this->timeout = intval($timeout);
}
return $this;
}
public function getTimeout()
{
return $this->timeout;
}
public function setAuthenticationPort($port)
{
if ((intval($port) > 0) && (intval($port) < 65536)) {
$this->authenticationPort = intval($port);
}
return $this;
}
public function getAuthenticationPort()
{
return $this->authenticationPort;
}
public function setAccountingPort($port)
{
if ((intval($port) > 0) && (intval($port) < 65536))
{
$this->accountingPort = intval($port);
}
return $this;
}
public function getResponsePacket()
{
return $this->radiusPacketReceived;
}
/**
* Alias of Radius::getAttribute()
*
* @param unknown $type
* @return NULL|unknown
*/
public function getReceivedAttribute($type)
{
return $this->getAttribute($type);
}
public function getReceivedAttributes()
{
return $this->attributesReceived;
}
public function getReadableReceivedAttributes()
{
$attributes = '';
if (isset($this->attributesReceived)) {
foreach($this->attributesReceived as $receivedAttr) {
$info = $this->getAttributesInfo($receivedAttr[0]);
$attributes .= sprintf('%s: ', $info[0]);
if (26 == $receivedAttr[0]) {
$vendorArr = $this->decodeVendorSpecificContent($receivedAttr[1]);
foreach($vendorArr as $vendor) {
$attributes .= sprintf('Vendor-Id: %s, Vendor-type: %s, Attribute-specific: %s',
$vendor[0], $vendor[1], $vendor[2]);
}
} else {
$attribues = $receivedAttr[1];
}
$attributes .= "<br>\n";
}
}
return $attributes;
}
public function getAttribute($type)
{
$value = null;
if (is_array($this->attributesReceived)) {
foreach($this->attributesReceived as $attr) {
if (intval($type) == $attr[0]) {
$value = $attr;
break;
}
}
}
return $value;
}
public function getRadiusPacketInfo($info_index)
{
if (isset($this->radiusPackets[intval($info_index)])) {
return $this->radiusPackets[intval($info_index)];
} else {
return '';
}
}
public function getAttributesInfo($info_index)
{
if (isset($this->attributesInfo[intval($info_index)])) {
return $this->attributesInfo[intval($info_index)];
} else {
return array('', '');
}
}
public function setAttribute($type, $value)
{
$index = -1;
if (is_array($this->attributesToSend)) {
foreach($this->attributesToSend as $i => $attr) {
if (is_array($attr)) {
$tmp = $attr[0];
} else {
$tmp = $attr;
}
if ($type == ord(substr($tmp, 0, 1))) {
$index = $i;
break;
}
}
}
$temp = null;
if (isset($this->attributesInfo[$type])) {
switch ($this->attributesInfo[$type][1]) {
case 'T':
// Text, 1-253 octets containing UTF-8 encoded ISO 10646 characters (RFC 2279).
$temp = chr($type) . chr(2 + strlen($value)) . $value;
break;
case 'S':
// String, 1-253 octets containing binary data (values 0 through 255 decimal, inclusive).
$temp = chr($type) . chr(2 + strlen($value)) . $value;
break;
case 'A':
// Address, 32 bit value, most significant octet first.
$ip = explode('.', $value);
$temp = chr($type) . chr(6) . chr($ip[0]) . chr($ip[1]) . chr($ip[2]) . chr($ip[3]);
break;
case 'I':
// Integer, 32 bit unsigned value, most significant octet first.
$temp = chr($type) . chr(6) .
chr(($value / (256 * 256 * 256)) % 256) .
chr(($value / (256 * 256)) % 256) .
chr(($value / (256)) % 256) .
chr($value % 256);
break;
case 'D':
// Time, 32 bit unsigned value, most significant octet first -- seconds since 00:00:00 UTC, January 1, 1970. (not used in this RFC)
$temp = null;
break;
default:
$temp = null;
}
}
if ($index > -1) {
if ($type == 26) { // vendor specific
$this->attributesToSend[$index][] = $temp;
$action = 'Added';
} else {
$this->attributesToSend[$index] = $temp;
$action = 'Modified';
}
} else {
$this->attributesToSend[] = ($type == 26 /* vendor specific */) ? array($temp) : $temp;
$action = 'Added';
}
$info = $this->getAttributesInfo($type);
$this->debugInfo("{$action} Attribute {$type} ({$info[0]}), format {$info[1]}, value <em>{$value}</em>");
return $this;
}
/**
* Get one or all set attributes to send
*
* @param int|null $type RADIUS attribute type, or null for all
* @return mixed array of attributes to send, or null if specific attribute not found, or
*/
public function getAttributesToSend($type = null)
{
if (is_array($this->attributesToSend)) {
if ($type == null) {
return $this->attributesToSend;
} else {
foreach($this->attributesToSend as $i => $attr) {
if (is_array($attr)) {
$tmp = $attr[0];
} else {
$tmp = $attr;
}
if ($type == ord(substr($tmp, 0, 1))) {
return $this->decodeAttribute(substr($tmp, 2), $type);
}
}
return null;
}
}
return array();
}
public function setVendorSpecificAttribute($vendorId, $attributeType, $attributeValue)
{
$data = pack('N', $vendorId);
$data .= chr($attributeType);
$data .= chr(2 + strlen($attributeValue));
$data .= $attributeValue;
$this->setAttribute(26, $data);
return $this;
}
public function removeAttribute($type)
{
$index = -1;
if (is_array($this->attributesToSend)) {
foreach($this->attributesToSend as $i => $attr) {
if (is_array($attr)) {
$tmp = $attr[0];
} else {
$tmp = $attr;
}
if ($type == ord(substr($tmp, 0, 1))) {
unset($this->attributesToSend[$i]);
break;
}
}
}
return $this;
}
public function resetAttributes()
{
$this->attributesToSend = null;
return $this;
}
public function resetVendorSpecificAttributes()
{
$this->removeAttribute(26);
return $this;
}
public function decodeVendorSpecificContent($rawValue)
{
$result = array();
$offset = 0;
$vendorId = (ord(substr($rawValue, 0, 1)) * 256 * 256 * 256) +
(ord(substr($rawValue, 1, 1)) * 256 * 256) +
(ord(substr($rawValue, 2, 1)) * 256) +
ord(substr($rawValue, 3, 1));
$offset += 4;
while ($offset < strlen($rawValue)) {
$vendorType = (ord(substr($rawValue, 0 + $offset, 1)));
$vendorLength = (ord(substr($rawValue, 1 + $offset, 1)));
$attributeSpecific = substr($rawValue, 2 + $offset, $vendorLength);
$result[] = array($vendorId, $vendorType, $attributeSpecific);
$offset += $vendorLength;
}
return $result;
}
public function accessRequest($username = '', $password = '', $timeout = 0, $state = null)
{
$this->clearDataReceived()
->clearError()
->setPacketType(self::TYPE_ACCESS_REQUEST);
if (0 < strlen($username)) {
$this->setUsername($username);
}
if (0 < strlen($password)) {
$this->setPassword($password);
}
if ($state !== null) {
$this->setAttribute(24, $state);
} else {
$this->setAttribute(6, 1); // 1=Login
}
if (intval($timeout) > 0) {
$this->setTimeout($timeout);
}
$packetData = $this->generateRadiusPacket();
$conn = $this->sendRadiusRequest($packetData);
if (!$conn) {
return false;
}
$receivedPacket = $this->readRadiusResponse($conn);
@fclose($conn);
if (!$receivedPacket) {
return false;
}
if (!$this->parseRadiusResponsePacket($receivedPacket)) {
return false;
}
if ($this->radiusPacketReceived == self::TYPE_ACCESS_REJECT) {
$this->errorCode = 3;
$this->errorMessage = 'Access rejected';
}
return (self::TYPE_ACCESS_ACCEPT == ($this->radiusPacketReceived));
}
public function accessRequestEapMsChapV2($username, $password)
{
/*
* RADIUS EAP MSCHAPv2 Process:
* > RADIUS ACCESS_REQUEST w/ EAP identity packet
* < ACCESS_CHALLENGE w/ MSCHAP challenge encapsulated in EAP request
* CHAP packet contains auth_challenge value
* Calculate encrypted password based on challenge for response
* > ACCESS_REQUEST w/ MSCHAP challenge response, peer_challenge &
* encrypted password encapsulated in an EAP response packet
* < ACCESS_CHALLENGE w/ MSCHAP success or failure in EAP packet.
* > ACCESS_REQUEST w/ EAP success packet if challenge was accepted
*
*/
$attributes = $this->getAttributesToSend();
$this->clearDataToSend()
->clearError()
->setPacketType(self::TYPE_ACCESS_REQUEST);
$this->attributesToSend = $attributes;
$eapPacket = EAPPacket::identity($username);
$this->setUsername($username)
->setAttribute(79, $eapPacket)
->setIncludeMessageAuthenticator();
$this->accessRequest();
if ($this->errorCode) {
return false;
} elseif ($this->radiusPacketReceived != self::TYPE_ACCESS_CHALLENGE) {
$this->errorCode = 102;
$this->errorMessage = 'Access-Request did not get Access-Challenge response';
return false;
}
$state = $this->getReceivedAttribute(24);
$eap = $this->getReceivedAttribute(79);
if ($eap == null) {
$this->errorCode = 102;
$this->errorMessage = 'EAP packet missing from MSCHAP v2 access response';
return false;
}
$eap = EAPPacket::fromString($eap);
if ($eap->type != EAPPacket::TYPE_EAP_MS_AUTH) {
$this->errorCode = 102;
$this->errorMessage = 'EAP type is not EAP_MS_AUTH in access response';
return false;
}
$chapPacket = MsChapV2Packet::fromString($eap->data);
if (!$chapPacket || $chapPacket->opcode != MsChapV2Packet::OPCODE_CHALLENGE) {
$this->errorCode = 102;
$this->errorMessage = 'MSCHAP v2 access response packet missing challenge';
return false;
}
$challenge = $chapPacket->challenge;
$chapId = $chapPacket->msChapId;
$msChapV2 = new \Crypt_CHAP_MSv2;
$msChapV2->username = $username;
$msChapV2->password = $password;
$msChapV2->chapid = $chapPacket->msChapId;
$msChapV2->authChallenge = $challenge;
$response = $msChapV2->challengeResponse();
$chapPacket->opcode = MsChapV2Packet::OPCODE_RESPONSE;
$chapPacket->response = $response;
$chapPacket->name = $username;
$chapPacket->challenge = $msChapV2->peerChallenge;
$eapPacket = EAPPacket::mschapv2($chapPacket, $chapId);
$this->clearDataToSend()
->setPacketType(self::TYPE_ACCESS_REQUEST)
->setUsername($username)
->setAttribute(79, $eapPacket)
->setIncludeMessageAuthenticator();
$resp = $this->accessRequest(null, null, 0, $state);
if ($this->errorCode) {
return false;
}
$eap = $this->getReceivedAttribute(79);
if ($eap == null) {
$this->errorCode = 102;
$this->errorMessage = 'EAP packet missing from MSCHAP v2 challenge response';
return false;
}
$eap = EAPPacket::fromString($eap);
if ($eap->type != EAPPacket::TYPE_EAP_MS_AUTH) {
$this->errorCode = 102;
$this->errorMessage = 'EAP type is not EAP_MS_AUTH in access response';
return false;
}
$chapPacket = MsChapV2Packet::fromString($eap->data);
if ($chapPacket->opcode != MsChapV2Packet::OPCODE_SUCCESS) {
$this->errorCode = 3;
$err = (!empty($chapPacket->response)) ? $chapPacket->response : 'General authentication failure';
if (preg_match('/E=(\d+)/', $chapPacket->response, $err)) {
switch($err[1]) {
case '691':
$err = 'Authentication failure, username or password incorrect.';
break;
case '646':
$err = 'Authentication failure, restricted logon hours.';
break;
case '647':
$err = 'Account disabled';
break;
case '648':
$err = 'Password expired';
break;
case '649':
$err = 'No dial in permission';
break;
}
}
$this->errorMessage = $err;
return false;
}
// got a success response - send success acknowledgement
$state = $this->getReceivedAttribute(24);
$chapPacket = new MsChapV2Packet();
$chapPacket->opcode = MsChapV2Packet::OPCODE_SUCCESS;
$eapPacket = EAPPacket::mschapv2($chapPacket, $chapId + 1);
$this->clearDataToSend()
->setPacketType(self::TYPE_ACCESS_REQUEST)
->setUsername($username)
->setAttribute(79, $eapPacket)
->setIncludeMessageAuthenticator();
$resp = $this->accessRequest(null, null, 0, $state);
if ($resp !== true) {
return false;
} else {
return true;
}
}
private function sendRadiusRequest($packetData)
{
$packetLen = strlen($packetData);
$conn = @fsockopen('udp://' . $this->server, $this->authenticationPort, $errno, $errstr);
if (!$conn) {
$this->errorCode = $errno;
$this->errorMessage = $errstr;
return false;
}
$sent = fwrite($conn, $packetData);
if (!$sent || $packetLen != $sent) {
$this->errorCode = 55; // CURLE_SEND_ERROR
$this->errorMessage = 'Failed to send UDP packet';
return false;
}
if ($this->debug) {
$this->debugInfo(
sprintf(
'<b>Packet type %d (%s) sent</b>',
$this->radiusPacket,
$this->getRadiusPacketInfo($this->radiusPacket)
)
);
foreach($this->attributesToSend as $attrs) {
if (!is_array($attrs)) {
$attrs = array($attrs);
}
foreach($attrs as $attr) {
$attrInfo = $this->getAttributesInfo(ord(substr($attr, 0, 1)));
$this->debugInfo(
sprintf(
'Attribute %d (%s), length (%d), format %s, value <em>%s</em>',
ord(substr($attr, 0, 1)),
$attrInfo[0],
ord(substr($attr, 1, 1)) - 2,
$attrInfo[1],
$this->decodeAttribute(substr($attr, 2), ord(substr($attr, 0, 1)))
)
);
}
}
}
return $conn;
}
private function readRadiusResponse($conn)
{
stream_set_blocking($conn, false);
$read = array($conn);
$write = null;
$except = null;
$receivedPacket = '';
$packetLen = null;
$elapsed = 0;
do {
// Loop until the entire packet is read. Even with small packets,
// not all data might get returned in one read on a non-blocking stream.
$t0 = microtime(true);
$changed = stream_select($read, $write, $except, $this->timeout);
$t1 = microtime(true);
if ($changed > 0) {
$data = fgets($conn, 1024);
// Try to read as much data from the stream in one pass until 4
// bytes are read. Once we have 4 bytes, we can determine the
// length of the RADIUS response to know when to stop reading.
if ($data === false) {
// recv could fail due to ICMP destination unreachable
$this->errorCode = 56; // CURLE_RECV_ERROR
$this->errorMessage = 'Failure with receiving network data';
return false;
}
$receivedPacket .= $data;
if (strlen($receivedPacket) < 4) {
// not enough data to get the size
// this will probably never happen
continue;
}
if ($packetLen == null) {
// first pass - decode the packet size from response
$packetLen = unpack('n', substr($receivedPacket, 2, 2));
$packetLen = (int)array_shift($packetLen);
if ($packetLen < 4 || $packetLen > 65507) {
$this->errorCode = 102;
$this->errorMessage = "Bad packet size in RADIUS response. Got {$packetLen}";
return false;
}
}
} elseif ($changed === false) {
$this->errorCode = 2;
$this->errorMessage = 'stream_select returned false';
return false;
} else {
$this->errorCode = 28; // CURLE_OPERATION_TIMEDOUT
$this->errorMessage = 'Timed out while waiting for RADIUS response';
return false;
}
$elapsed += ($t1 - $t0);
} while ($elapsed < $this->timeout && strlen($receivedPacket) < $packetLen);
return $receivedPacket;
}
private function parseRadiusResponsePacket($packet)
{
$this->radiusPacketReceived = intval(ord(substr($packet, 0, 1)));
$this->debugInfo(sprintf(
'<b>Packet type %d (%s) received</b>',
$this->radiusPacketReceived,
$this->getRadiusPacketInfo($this->getResponsePacket())
));
if ($this->radiusPacketReceived > 0) {
$this->identifierReceived = intval(ord(substr($packet, 1, 1)));
$packetLenRx = unpack('n', substr($packet, 2, 2));
$packetLenRx = array_shift($packetLenRx);
$this->responseAuthenticator = bin2hex(substr($packet, 4, 16));
if ($packetLenRx > 20) {
$attrContent = substr($packet, 20);
} else {
$attrContent = '';
}
$authCheck = md5(
substr($packet, 0, 4) .
$this->getRequestAuthenticator() .
$attrContent .
$this->getSecret()
);
if ($authCheck !== $this->responseAuthenticator) {
$this->errorCode = 101;
$this->errorMessage = 'Response authenticator in received packet did not match expected value';
return false;
}
while (strlen($attrContent) > 2) {
$attrType = intval(ord(substr($attrContent, 0, 1)));
$attrLength = intval(ord(substr($attrContent, 1, 1)));
$attrValueRaw = substr($attrContent, 2, $attrLength - 2);
$attrContent = substr($attrContent, $attrLength);
$attrValue = $this->decodeAttribute($attrValueRaw, $attrType);
$attr = $this->getAttributesInfo($attrType);
if (26 == $attrType) {
$vendorArr = $this->decodeVendorSpecificContent($attrValue);
foreach($vendorArr as $vendor) {
$this->debugInfo(
sprintf(
'Attribute %d (%s), length %d, format %s, Vendor-Id: %d, Vendor-type: %s, Attribute-specific: %s',
$attrType, $attr[0], $attrLength - 2,
$attr[1], $vendor[0], $vendor[1], $vendor[2]
)
);
}
} else {
$this->debugInfo(
sprintf(
'Attribute %d (%s), length %d, format %s, value <em>%s</em>',
$attrType, $attr[0], $attrLength - 2, $attr[1], $attrValue
)
);
}
// TODO: check message authenticator
$this->attributesReceived[] = array($attrType, $attrValue);
}
} else {
$this->errorCode = 100;
$this->errorMessage = 'Invalid response packet received';
return false;
}
return true;
}
public function generateRadiusPacket()
{
$hasAuthenticator = false;
$attrContent = '';
$len = 0;
$offset = null;
foreach($this->attributesToSend as $i => $attr) {
$len = strlen($attrContent);
if (is_array($attr)) {
// vendor specific (could have multiple attributes)
$attrContent .= implode('', $attr);
} else {
if (ord($attr[0]) == 80) {
// If Message-Authenticator is set, note offset so it can be updated
$hasAuthenticator = true;
$offset = $len + 2; // current length + type(1) + length(1)
}
$attrContent .= $attr;
}
}
$attrLen = strlen($attrContent);
$packetLen = 4; // Radius packet code + Identifier + Length high + Length low
$packetLen += strlen($this->getRequestAuthenticator()); // Request-Authenticator
$packetLen += $attrLen; // Attributes
$packetData = chr($this->radiusPacket);
$packetData .= pack('C', $this->getNextIdentifier());
$packetData .= pack('n', $packetLen);
$packetData .= $this->getRequestAuthenticator();
$packetData .= $attrContent;
if ($hasAuthenticator && !is_null($offset)) {
$messageAuthenticator = hash_hmac('md5', $packetData, $this->secret, true);
// calculate packet hmac, replace hex 0's with actual hash
for ($i = 0; $i < strlen($messageAuthenticator); ++$i) {
$packetData[20 + $offset + $i] = $messageAuthenticator[$i];
}
}
return $packetData;
}
public function setNextIdentifier($identifierToSend = 0)
{
$id = (int)$identifierToSend;
$this->identifierToSend = $id - 1;
return $this;
}
public function getNextIdentifier()
{
$this->identifierToSend = (($this->identifierToSend + 1) % 256);
return $this->identifierToSend;
}
private function generateRequestAuthenticator()
{
$this->requestAuthenticator = '';
for ($c = 0; $c <= 15; ++$c) {
$this->requestAuthenticator .= chr(rand(1, 255));
}
return $this;
}
public function setRequestAuthenticator($requestAuthenticator)
{
if (strlen($requestAuthenticator) != 16) {
return false;
}
$this->requestAuthenticator = $requestAuthenticator;
return $this;
}
public function getRequestAuthenticator()
{
return $this->requestAuthenticator;
}
protected function clearDataToSend()
{
$this->radiusPacket = 0;
$this->attributesToSend = null;
return $this;
}
protected function clearDataReceived()
{
$this->radiusPacketReceived = 0;
$this->attributesReceived = null;
return $this;
}
public function setPacketType($type)
{
$this->radiusPacket = $type;
return $this;
}
private function clearError()
{
$this->errorCode = 0;
$this->errorMessage = '';
return $this;
}
protected function debugInfo($message)
{
if ($this->debug) {
echo date('Y-m-d H:i:s').' DEBUG: ';
echo $message;
echo "<br />\n";
flush();
}
}
private function decodeAttribute($rawValue, $attributeFormat)
{
$value = null;
if (isset($this->attributesInfo[$attributeFormat])) {
switch ($this->attributesInfo[$attributeFormat][1]) {
case 'T':
$value = $rawValue;
break;
case 'S':
$value = $rawValue;
break;
case 'A':
$value = ord(substr($rawValue, 0, 1)) . '.' .
ord(substr($rawValue, 1, 1)) . '.' .
ord(substr($rawValue, 2, 1)) . '.' .
ord(substr($rawValue, 3, 1));
break;
case 'I':
$value = (ord(substr($rawValue, 0, 1)) * 256 * 256 * 256) +
(ord(substr($rawValue, 1, 1)) * 256 * 256) +
(ord(substr($rawValue, 2, 1)) * 256) +
ord(substr($rawValue, 3, 1));
break;
case 'D':
$value = null;
break;
default:
$value = null;
}
}
return $value;
}
}