0x7e || strstr('*()\/', $ch)) { $ch = '\\' . dechex($ord); } $ret .= $ch; } return $ret; } /** * @param string $dn The DN to parse * @param array $keys An optional array to receive DN keys (e.g. CN, OU, DC, ...) * @param array $vals An optional array to receive DN values * @return bool True if the DN was successfully parsed or false if the string is not a valid DN. */ public static function explodeDn($dn, array &$keys = null, array &$vals = null) { /* This is a classic state machine parser. Each iteration of the * loop processes one character. State 1 collects the key. When equals (=) * is encountered the state changes to 2 where the value is collected * until a comma (,) or semicolon (;) is encountered after which we switch back * to state 1. If a backslash (\) is encountered, state 3 is used to collect the * following character without engaging the logic of other states. */ $key = null; $slen = strlen($dn); $state = 1; $ko = $vo = 0; for ($di = 0; $di <= $slen; $di++) { $ch = $di == $slen ? 0 : $dn[$di]; switch ($state) { case 1: // collect key if ($ch === '=') { $key = trim(substr($dn, $ko, $di - $ko)); if ($keys !== null) { $keys[] = $key; } $state = 2; $vo = $di + 1; } else if ($ch === ',' || $ch === ';') { return false; } break; case 2: // collect value if ($ch === '\\') { $state = 3; } else if ($ch === ',' || $ch === ';' || $ch === 0) { if ($vals !== null) { $vals[] = trim(substr($dn, $vo, $di - $vo)); } $state = 1; $ko = $di + 1; } else if ($ch === '=') { return false; } break; case 3: // escaped $state = 2; break; } } return $state === 1 && $ko > 0; } /** * @param array $options Options used in connecting, binding, etc. * @return void */ public function __construct(array $options = array()) { $this->setOptions($options); } /** * Sets the options used in connecting, binding, etc. * * Valid option keys: * host * port * useSsl * username * password * bindRequiresDn * baseDn * accountCanonicalForm * accountDomainName * accountDomainNameShort * accountFilterFormat * allowEmptyPassword * useStartTls * optRefferals * * @param array $options Options used in connecting, binding, etc. * @return Zend_Ldap Provides a fluent interface * @throws Zend_Ldap_Exception */ public function setOptions(array $options) { $permittedOptions = array( 'host' => null, 'port' => null, 'useSsl' => null, 'username' => null, 'password' => null, 'bindRequiresDn' => null, 'baseDn' => null, 'accountCanonicalForm' => null, 'accountDomainName' => null, 'accountDomainNameShort' => null, 'accountFilterFormat' => null, 'allowEmptyPassword' => null, 'useStartTls' => null, 'optReferrals' => null, ); $diff = array_diff_key($options, $permittedOptions); if ($diff) { list($key, $val) = each($diff); require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, "Unknown Zend_Ldap option: $key"); } foreach ($permittedOptions as $key => $val) { if (!array_key_exists($key, $options)) { $options[$key] = null; } else { /* Enforce typing. This eliminates issues like Zend_Config_Ini * returning '1' as a string (ZF-3163). */ switch ($key) { case 'port': case 'accountCanonicalForm': $options[$key] = (int)$options[$key]; break; case 'useSsl': case 'bindRequiresDn': case 'allowEmptyPassword': case 'useStartTls': case 'optReferrals': $val = $options[$key]; $options[$key] = $val === true || $val === '1' || strcasecmp($val, 'true') == 0; break; } } } $this->_options = $options; return $this; } /** * @return array The current options. */ public function getOptions() { return $this->_options; } /** * @return resource The raw LDAP extension resource. */ public function getResource() { /** * @todo by reference? */ return $this->_resource; } /** * @return string The hostname of the LDAP server being used to authenticate accounts */ protected function _getHost() { return $this->_options['host']; } /** * @return int The port of the LDAP server or 0 to indicate that no port value is set */ protected function _getPort() { if ($this->_options['port']) return $this->_options['port']; return 0; } /** * @return string The default acctname for binding */ protected function _getUsername() { return $this->_options['username']; } /** * @return string The default password for binding */ protected function _getPassword() { return $this->_options['password']; } /** * @return boolean The default SSL / TLS encrypted transport control */ protected function _getUseSsl() { return $this->_options['useSsl']; } /** * @return string The default base DN under which objects of interest are located */ protected function _getBaseDn() { return $this->_options['baseDn']; } /** * @return string Either ACCTNAME_FORM_BACKSLASH, ACCTNAME_FORM_PRINCIPAL or ACCTNAME_FORM_USERNAME indicating the form usernames should be canonicalized to. */ protected function _getAccountCanonicalForm() { /* Account names should always be qualified with a domain. In some scenarios * using non-qualified account names can lead to security vulnerabilities. If * no account canonical form is specified, we guess based in what domain * names have been supplied. */ $accountCanonicalForm = $this->_options['accountCanonicalForm']; if (!$accountCanonicalForm) { $accountDomainName = $this->_options['accountDomainName']; $accountDomainNameShort = $this->_options['accountDomainNameShort']; if ($accountDomainNameShort) { $accountCanonicalForm = Zend_Ldap::ACCTNAME_FORM_BACKSLASH; } else if ($accountDomainName) { $accountCanonicalForm = Zend_Ldap::ACCTNAME_FORM_PRINCIPAL; } else { $accountCanonicalForm = Zend_Ldap::ACCTNAME_FORM_USERNAME; } } return $accountCanonicalForm; } /** * @return string A format string for building an LDAP search filter to match an account */ protected function _getAccountFilterFormat() { return $this->_options['accountFilterFormat']; } /** * @return string The LDAP search filter for matching directory accounts */ protected function _getAccountFilter($acctname) { $this->_splitName($acctname, $dname, $aname); $accountFilterFormat = $this->_getAccountFilterFormat(); $aname = Zend_Ldap::filterEscape($aname); if ($accountFilterFormat) return sprintf($accountFilterFormat, $aname); if (!$this->_options['bindRequiresDn']) { // is there a better way to detect this? return "(&(objectClass=user)(sAMAccountName=$aname))"; } return "(&(objectClass=posixAccount)(uid=$aname))"; } /** * @param string $name The name to split * @param string $dname The resulting domain name (this is an out parameter) * @param string $aname The resulting account name (this is an out parameter) */ protected function _splitName($name, &$dname, &$aname) { $dname = NULL; $aname = $name; $pos = strpos($name, '@'); if ($pos) { $dname = substr($name, $pos + 1); $aname = substr($name, 0, $pos); } else { $pos = strpos($name, '\\'); if ($pos) { $dname = substr($name, 0, $pos); $aname = substr($name, $pos + 1); } } } /** * @param string $acctname The name of the account * @return string The DN of the specified account * @throws Zend_Ldap_Exception */ protected function _getAccountDn($acctname) { if (Zend_Ldap::explodeDn($acctname)) return $acctname; $acctname = $this->getCanonicalAccountName($acctname, Zend_Ldap::ACCTNAME_FORM_USERNAME); $acct = $this->_getAccount($acctname, array('dn')); return $acct['dn']; } /** * @param string $dname The domain name to check * @return bool */ protected function _isPossibleAuthority($dname) { if ($dname === null) return true; $accountDomainName = $this->_options['accountDomainName']; $accountDomainNameShort = $this->_options['accountDomainNameShort']; if ($accountDomainName === null && $accountDomainNameShort === null) return true; if (strcasecmp($dname, $accountDomainName) == 0) return true; if (strcasecmp($dname, $accountDomainNameShort) == 0) return true; return false; } /** * @param string $acctname The name to canonicalize * @param int $type The desired form of canonicalization * @return string The canonicalized name in the desired form * @throws Zend_Ldap_Exception */ public function getCanonicalAccountName($acctname, $form = 0) { $this->_splitName($acctname, $dname, $uname); if (!$this->_isPossibleAuthority($dname)) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, "Binding domain is not an authority for user: $acctname", Zend_Ldap_Exception::LDAP_X_DOMAIN_MISMATCH); } if ($form === Zend_Ldap::ACCTNAME_FORM_DN) return $this->_getAccountDn($acctname); if (!$uname) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, "Invalid account name syntax: $acctname"); } $uname = strtolower($uname); if ($form === 0) $form = $this->_getAccountCanonicalForm(); switch ($form) { case Zend_Ldap::ACCTNAME_FORM_USERNAME: return $uname; case Zend_Ldap::ACCTNAME_FORM_BACKSLASH: $accountDomainNameShort = $this->_options['accountDomainNameShort']; if (!$accountDomainNameShort) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, 'Option required: accountDomainNameShort'); } return "$accountDomainNameShort\\$uname"; case Zend_Ldap::ACCTNAME_FORM_PRINCIPAL: $accountDomainName = $this->_options['accountDomainName']; if (!$accountDomainName) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, 'Option required: accountDomainName'); } return "$uname@$accountDomainName"; default: /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, "Unknown canonical name form: $form"); } } /** * @param array $attrs An array of names of desired attributes * @return array An array of the attributes representing the account * @throws Zend_Ldap_Exception */ private function _getAccount($acctname, array $attrs = null) { $baseDn = $this->_getBaseDn(); if (!$baseDn) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, 'Base DN not set'); } $accountFilter = $this->_getAccountFilter($acctname); if (!$accountFilter) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, 'Invalid account filter'); } if (!is_resource($this->_resource)) $this->bind(); $resource = $this->_resource; $str = $accountFilter; $code = 0; /** * @todo break out search operation into simple function (private for now) */ if (!extension_loaded('ldap')) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, 'LDAP extension not loaded'); } $result = @ldap_search($resource, $baseDn, $accountFilter, $attrs); if (is_resource($result) === true) { $count = @ldap_count_entries($resource, $result); if ($count == 1) { $entry = @ldap_first_entry($resource, $result); if ($entry) { $acct = array('dn' => @ldap_get_dn($resource, $entry)); $name = @ldap_first_attribute($resource, $entry, $berptr); while ($name) { $data = @ldap_get_values_len($resource, $entry, $name); $acct[$name] = $data; $name = @ldap_next_attribute($resource, $entry, $berptr); } @ldap_free_result($result); return $acct; } } else if ($count == 0) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; $code = Zend_Ldap_Exception::LDAP_NO_SUCH_OBJECT; } else { /** * @todo limit search to 1 record and remove some of this logic? */ $resource = null; $str = "$accountFilter: Unexpected result count: $count"; /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; $code = Zend_Ldap_Exception::LDAP_OPERATIONS_ERROR; } @ldap_free_result($result); } /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception($resource, $str, $code); } /** * @return Zend_Ldap Provides a fluent interface */ public function disconnect() { if (is_resource($this->_resource)) { if (!extension_loaded('ldap')) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, 'LDAP extension not loaded'); } @ldap_unbind($this->_resource); } $this->_resource = null; return $this; } /** * @param string $host The hostname of the LDAP server to connect to * @param int $port The port number of the LDAP server to connect to * @return Zend_Ldap Provides a fluent interface * @throws Zend_Ldap_Exception */ public function connect($host = null, $port = 0, $useSsl = false) { if ($host === null) $host = $this->_getHost(); if ($port === 0) $port = $this->_getPort(); if ($useSsl === false) $useSsl = $this->_getUseSsl(); if (!$host) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, 'A host parameter is required'); } /* To connect using SSL it seems the client tries to verify the server * certificate by default. One way to disable this behavior is to set * 'TLS_REQCERT never' in OpenLDAP's ldap.conf and restarting Apache. Or, * if you really care about the server's cert you can put a cert on the * web server. */ $url = $useSsl ? "ldaps://$host" : "ldap://$host"; if ($port) { $url .= ":$port"; } /* Because ldap_connect doesn't really try to connect, any connect error * will actually occur during the ldap_bind call. Therefore, we save the * connect string here for reporting it in error handling in bind(). */ $this->_connectString = $url; $this->disconnect(); if (!extension_loaded('ldap')) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, 'LDAP extension not loaded'); } /* Only OpenLDAP 2.2 + supports URLs so if SSL is not requested, just * use the old form. */ $resource = $useSsl ? @ldap_connect($url) : @ldap_connect($host, $port); if (is_resource($resource) === true) { $this->_resource = $resource; $optReferrals = $this->_options['optReferrals'] ? 1 : 0; if (@ldap_set_option($resource, LDAP_OPT_PROTOCOL_VERSION, 3) && @ldap_set_option($resource, LDAP_OPT_REFERRALS, $optReferrals)) { if ($useSsl || $this->_options['useStartTls'] !== true || @ldap_start_tls($resource)) { return $this; } } /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; $zle = new Zend_Ldap_Exception($resource, "$host:$port"); $this->disconnect(); throw $zle; } /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception("Failed to connect to LDAP server: $host:$port"); } /** * @param string $username The username for authenticating the bind * @param string $password The password for authenticating the bind * @return Zend_Ldap Provides a fluent interface * @throws Zend_Ldap_Exception */ public function bind($username = null, $password = null) { $moreCreds = true; if ($username === null) { $username = $this->_getUsername(); $password = $this->_getPassword(); $moreCreds = false; } if ($username === NULL) { /* Perform anonymous bind */ $password = NULL; } else { /* Check to make sure the username is in DN form. */ if (!Zend_Ldap::explodeDn($username)) { if ($this->_options['bindRequiresDn']) { /* moreCreds stops an infinite loop if _getUsername does not * return a DN and the bind requires it */ if ($moreCreds) { try { $username = $this->_getAccountDn($username); } catch (Zend_Ldap_Exception $zle) { /** * @todo Temporary measure to deal with exception thrown for ldap extension not loaded */ if (strpos($zle->getMessage(), 'LDAP extension not loaded') !== false) { throw $zle; } // end temporary measure switch ($zle->getCode()) { case Zend_Ldap_Exception::LDAP_NO_SUCH_OBJECT: case Zend_Ldap_Exception::LDAP_X_DOMAIN_MISMATCH: throw $zle; } throw new Zend_Ldap_Exception(null, 'Failed to retrieve DN for account: ' . $zle->getMessage(), Zend_Ldap_Exception::LDAP_OPERATIONS_ERROR); } } else { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, 'Binding requires username in DN form'); } } else { $username = $this->getCanonicalAccountName($username, Zend_Ldap::ACCTNAME_FORM_PRINCIPAL); } } } if (!is_resource($this->_resource)) $this->connect(); if ($username !== null && $password === '' && $this->_options['allowEmptyPassword'] !== true) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; $zle = new Zend_Ldap_Exception(null, 'Empty password not allowed - see allowEmptyPassword option.'); } else { if (@ldap_bind($this->_resource, $username, $password)) return $this; $message = $username === null ? $this->_connectString : $username; /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; switch (Zend_Ldap_Exception::getLdapCode($this)) { case Zend_Ldap_Exception::LDAP_SERVER_DOWN: /* If the error is related to establishing a connection rather than binding, * the connect string is more informative than the username. */ $message = $this->_connectString; } $zle = new Zend_Ldap_Exception($this->_resource, $message); } $this->disconnect(); throw $zle; } }