. * * * @author: SysCo/al * @author: Drew Phillips * @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 .= "
\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 {$value}"); 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( 'Packet type %d (%s) sent', $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 %s', 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( 'Packet type %d (%s) received', $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 %s', $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 "
\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; } }