Source for file ldap.php

Documentation is available at ldap.php

  1. <?php
  2.  
  3. /**
  4.  * Change password LDAP backend
  5.  *
  6.  * @copyright &copy; 2005-2006 The SquirrelMail Project Team
  7.  * @license http://opensource.org/licenses/gpl-license.php GNU Public License
  8.  * @version $Id: ldap.php,v 1.15 2006/07/15 12:01:09 tokul Exp $
  9.  * @package plugins
  10.  * @subpackage change_password
  11.  */
  12.  
  13. /**
  14.  * do not allow to call this file directly
  15.  */
  16. if ((isset($_SERVER['SCRIPT_FILENAME']&& $_SERVER['SCRIPT_FILENAME'== __FILE__||
  17.      (isset($HTTP_SERVER_SERVER['SCRIPT_FILENAME']&& $HTTP_SERVER_SERVER['SCRIPT_FILENAME'== __FILE__) ) {
  18.     header("Location: ../../../src/login.php");
  19.     die();
  20. }
  21.  
  22. /** load required functions */
  23.  
  24. /** sqimap_get_user_server() function */
  25. include_once(SM_PATH '../functions/imap_general.php');
  26.  
  27. /** get imap server and username globals */
  28. global $imapServerAddress$username;
  29.  
  30. /** Default plugin configuration.*/
  31. /**
  32.  * Address of LDAP server.
  33.  * You can use any URL format that is supported by your LDAP extension.
  34.  * Examples:
  35.  * <ul>
  36.  *   <li>'ldap.example.com' - connect to server on ldap.example.com address
  37.  *   <li>'ldaps://ldap.example.com' - connect to server on ldap.example.com address
  38.  *   and use SSL encrypted connection to default LDAPs port.
  39.  * </ul>
  40.  * defaults to imap server address.
  41.  * @link http://www.php.net/ldap-connect
  42.  * @global string $cpw_ldap_server 
  43.  */
  44. global $cpw_ldap_server;
  45. $cpw_ldap_server=$imapServerAddress;
  46.  
  47. /**
  48.  * Port of LDAP server.
  49.  * Used only when $cpw_ldap_server specifies IP address or DNS name.
  50.  * @global integer $cpw_ldap_port 
  51.  */
  52. global $cpw_ldap_port;
  53. $cpw_ldap_port=389;
  54.  
  55. /**
  56.  * LDAP basedn that is used for binding to LDAP server.
  57.  * this option must be set to correct value.
  58.  * @global string $cpw_ldap_basedn; 
  59.  */
  60. global $cpw_ldap_basedn;
  61. $cpw_ldap_basedn='';
  62.  
  63. /**
  64.  * LDAP connection options
  65.  * @link http://www.php.net/ldap-set-option
  66.  * @global array $cpw_ldap_connect_opts 
  67.  */
  68. global $cpw_ldap_connect_opts;
  69. $cpw_ldap_connect_opts=array();
  70.  
  71. /**
  72.  * Controls use of starttls on LDAP connection.
  73.  * Requires PHP 4.2+, PHP LDAP extension with SSL support and
  74.  * PROTOCOL_VERSION => 3 setting in $cpw_ldap_connect_opts
  75.  * @global boolean $cpw_ldap_use_tls 
  76.  */
  77. global $cpw_ldap_use_tls;
  78. $cpw_ldap_use_tls=false;
  79.  
  80. /**
  81.  * BindDN that should be able to search LDAP directory and find DN used by user.
  82.  * Uses anonymous bind if set to empty string. You should not use DN with write
  83.  * access to LDAP directory here. Write access is not required.
  84.  * @global string $cpw_ldap_binddn 
  85.  */
  86. global $cpw_ldap_binddn;
  87. $cpw_ldap_binddn='';
  88.  
  89. /**
  90.  * password used for $cpw_ldap_binddn
  91.  * @global string $cpw_ldap_bindpw 
  92.  */
  93. global $cpw_ldap_bindpw;
  94. $cpw_ldap_bindpw='';
  95.  
  96. /**
  97.  * BindDN that should be able to change password.
  98.  * WARNING: sometimes user has enough privileges to change own password.
  99.  * If you leave default value, plugin will try to connect with DN that
  100.  * is detected in $cpw_ldap_username_attr=$username search and current
  101.  * user password will be used for authentication.
  102.  * @global string $cpw_ldap_admindn 
  103.  */
  104. global $cpw_ldap_admindn;
  105. $cpw_ldap_admindn='';
  106.  
  107. /**
  108.  * password used for $cpw_ldap_admindn
  109.  * @global string $cpw_ldap_adminpw 
  110.  */
  111. global $cpw_ldap_adminpw;
  112. $cpw_ldap_adminpw='';
  113.  
  114. /**
  115.  * LDAP attribute that stores username.
  116.  * username entry should be unique for $cpw_ldap_basedn
  117.  * @global string $cpw_ldap_userid_attr 
  118.  */
  119. global $cpw_ldap_userid_attr;
  120. $cpw_ldap_userid_attr='uid';
  121.  
  122. /**
  123.  * crypto that is used to encode new password
  124.  * If set to empty string, system tries to keep same encoding/hashing algorithm
  125.  * @global string $cpw_ldap_default_crypto 
  126.  */
  127. global $cpw_ldap_default_crypto;
  128. $cpw_ldap_default_crypto='';
  129.  
  130. /** end of default config */
  131.  
  132. /** configuration overrides from config file */
  133. if (isset($cpw_ldap['server'])) $cpw_ldap_server=$cpw_ldap['server'];
  134. if (isset($cpw_ldap['port'])) $cpw_ldap_port=$cpw_ldap['port'];
  135. if (isset($cpw_ldap['basedn'])) $cpw_ldap_basedn=$cpw_ldap['basedn'];
  136. if (isset($cpw_ldap['connect_opts'])) $cpw_ldap_connect_opts=$cpw_ldap['connect_opts'];
  137. if (isset($cpw_ldap['use_tls'])) $cpw_ldap_use_tls=$cpw_ldap['use_tls'];
  138. if (isset($cpw_ldap['binddn'])) $cpw_ldap_binddn=$cpw_ldap['binddn'];
  139. if (isset($cpw_ldap['bindpw'])) $cpw_ldap_bindpw=$cpw_ldap['bindpw'];
  140. if (isset($cpw_ldap['admindn'])) $cpw_ldap_admindn=$cpw_ldap['admindn'];
  141. if (isset($cpw_ldap['adminpw'])) $cpw_ldap_adminpw=$cpw_ldap['adminpw'];
  142. if (isset($cpw_ldap['userid_attr'])) $cpw_ldap_userid_attr=$cpw_ldap['userid_attr'];
  143. if (isset($cpw_ldap['default_crypto'])) $cpw_ldap_default_crypto=$cpw_ldap['default_crypto'];
  144.  
  145. /** make sure that setting does not contain mapping */
  146. $cpw_ldap_server=sqimap_get_user_server($cpw_ldap_server,$username);
  147.  
  148. /**
  149.  * Adding plugin hooks
  150.  */
  151. global $squirrelmail_plugin_hooks;
  152. $squirrelmail_plugin_hooks['change_password_dochange']['ldap'=
  153.         'cpw_ldap_dochange';
  154. $squirrelmail_plugin_hooks['change_password_init']['ldap'=
  155.         'cpw_ldap_init';
  156.  
  157. /**
  158.  * Makes sure that required functions and configuration options are set.
  159.  */
  160. function cpw_ldap_init({
  161.     global $oTemplate$cpw_ldap_basedn;
  162.  
  163.     // set initial value for error tracker
  164.     $cpw_ldap_initerr=false;
  165.  
  166.     // check for ldap support in php
  167.     if (function_exists('ldap_connect')) {
  168.         error_box(_("Current configuration requires LDAP support in PHP."));
  169.         $cpw_ldap_initerr=true;
  170.     }
  171.  
  172.     // chech required configuration settings.
  173.     if ($cpw_ldap_basedn==''{
  174.         error_box(_("Plugin is not configured correctly."));
  175.         $cpw_ldap_initerr=true;
  176.     }
  177.  
  178.     // if error var is positive, close html and stop execution
  179.     if ($cpw_ldap_initerr{
  180.         $oTemplate->display('footer.tpl');
  181.         exit;
  182.     }
  183. }
  184.  
  185.  
  186. /**
  187.  * Changes password. Main function attached to hook
  188.  * @param array $data The username/curpw/newpw data.
  189.  * @return array Array of error messages.
  190.  */
  191. function cpw_ldap_dochange($data{
  192.     global $cpw_ldap_server$cpw_ldap_port$cpw_ldap_basedn,
  193.  
  194.     // unfortunately, we can only pass one parameter to a hook function,
  195.     // so we have to pass it as an array.
  196.     $username $data['username'];
  197.     $curpw $data['curpw'];
  198.     $newpw $data['newpw'];
  199.  
  200.     // globalize current password.
  201.  
  202.     $msgs array();
  203.  
  204.     /**
  205.      * connect to LDAP server
  206.      * hide ldap_connect() function call errors, because they are processed in script.
  207.      * any script execution error is treated as critical, error messages are dumped
  208.      * to $msgs and LDAP connection is closed with ldap_unbind(). all ldap_unbind()
  209.      * errors are suppressed. Any other error suppression should be explained.
  210.      */
  211.     $cpw_ldap_con=@ldap_connect($cpw_ldap_server);
  212.  
  213.     if ($cpw_ldap_con{
  214.         $cpw_ldap_con_err=false;
  215.  
  216.         // set connection options
  217.         if (is_array($cpw_ldap_connect_opts&& $cpw_ldap_connect_opts!=array()) {
  218.             // ldap_set_option() is available only with openldap 2.x and netscape directory sdk.
  219.             if (function_exists('ldap_set_option')) {
  220.                 foreach ($cpw_ldap_connect_opts as $opt => $value{
  221.                     // Make sure that constant is defined defore using it.
  222.                     if (defined('LDAP_OPT_' $opt)) {
  223.                         // ldap_set_option() should not produce E_NOTICE or E_ALL errors and does not modify ldap_error().
  224.                         // leave it without @ in order to see any weird errors
  225.                         if (ldap_set_option($cpw_ldap_con,constant('LDAP_OPT_' $opt),$value)) {
  226.                             // set error message
  227.                             array_push($msgs,sprintf(_("Setting of LDAP connection option %s to value %s failed."),$opt,$value));
  228.                             $cpw_ldap_con_err=true;
  229.                         }
  230.                     else {
  231.                         array_push($msgs,sprintf(_("Incorrect LDAP connection option: %s"),$opt));
  232.                         $cpw_ldap_con_err=true;
  233.                     }
  234.                 }
  235.             else {
  236.                 array_push($msgs,_("Current PHP LDAP extension does not allow use of ldap_set_option() function."));
  237.                 $cpw_ldap_con_err=true;
  238.             }
  239.         }
  240.  
  241.         // check for connection errors and stop execution if something is wrong
  242.         if ($cpw_ldap_con_err{
  243.             @ldap_unbind($cpw_ldap_con);
  244.             return $msgs;
  245.         }
  246.  
  247.         // enable ldap starttls
  248.         if ($cpw_ldap_use_tls &&
  249.             check_php_version(4,2,0&&
  250.             isset($cpw_ldap_connect_opts['PROTOCOL_VERSION']&&
  251.             $cpw_ldap_connect_opts['PROTOCOL_VERSION']>=&&
  252.             function_exists('ldap_start_tls')) {
  253.             // suppress ldap_start_tls errors and process error messages
  254.             if (@ldap_start_tls($cpw_ldap_con)) {
  255.                 array_push($msgs,
  256.                            _("Unable to use TLS."),
  257.                            sprintf(_("Error: %s"),ldap_error($cpw_ldap_con)));
  258.                 $cpw_ldap_con_err=true;
  259.             }
  260.         elseif ($cpw_ldap_use_tls{
  261.             array_push($msgs,_("Unable to use LDAP TLS in current setup."));
  262.             $cpw_ldap_con_err=true;
  263.         }
  264.  
  265.         // check for connection errors and stop execution if something is wrong
  266.         if ($cpw_ldap_con_err{
  267.             @ldap_unbind($cpw_ldap_con);
  268.             return $msgs;
  269.         }
  270.  
  271.         /**
  272.          * Bind to LDAP (use anonymous bind or unprivileged DN) in order to get user's DN
  273.          * hide ldap_bind() function call errors, because errors are processed in script
  274.          */
  275.         if ($cpw_ldap_binddn!=''{
  276.             // authenticated bind
  277.             $cpw_ldap_binding=@ldap_bind($cpw_ldap_con,$cpw_ldap_binddn,$cpw_ldap_bindpw);
  278.         else {
  279.             // anonymous bind
  280.             $cpw_ldap_binding=@ldap_bind($cpw_ldap_con);
  281.         }
  282.  
  283.         // check ldap_bind errors
  284.         if ($cpw_ldap_binding{
  285.             array_push($msgs,
  286.                        _("Unable to bind to LDAP server."),
  287.                        sprintf(_("Server replied: %s"),ldap_error($cpw_ldap_con)));
  288.             @ldap_unbind($cpw_ldap_con);
  289.             return $msgs;
  290.         }
  291.  
  292.         // find userdn
  293.         $cpw_ldap_search_err=cpw_ldap_uid_search($cpw_ldap_con,$cpw_ldap_basedn,$msgs,$cpw_ldap_res,$cpw_ldap_userdn);
  294.  
  295.         // check for search errors and stop execution if something is wrong
  296.         if ($cpw_ldap_search_err{
  297.             @ldap_unbind($cpw_ldap_con);
  298.             return $msgs;
  299.         }
  300.  
  301.         /**
  302.          * unset $cpw_ldap_res2 variable, if such var exists.
  303.          * $cpw_ldap_res2 object can be set in two places and second place checks,
  304.          * if object was created in first place. if variable name matches (somebody
  305.          * uses $cpw_ldap_res2 in code or globals), incorrect validation might
  306.          * cause script errors.
  307.          */
  308.         if (isset($cpw_ldap_res2)) unset($cpw_ldap_res2);
  309.  
  310.         // rebind as userdn or admindn
  311.         if ($cpw_ldap_admindn!=''{
  312.             // admindn bind
  313.             $cpw_ldap_binding=@ldap_bind($cpw_ldap_con,$cpw_ldap_admindn,$cpw_ldap_adminpw);
  314.  
  315.             if ($cpw_ldap_binding{
  316.                 // repeat search in order to get password info. Password info should be unavailable in unprivileged bind.
  317.                 $cpw_ldap_search_err=cpw_ldap_uid_search($cpw_ldap_con,$cpw_ldap_basedn,$msgs,$cpw_ldap_res2,$cpw_ldap_userdn);
  318.  
  319.                 // check for connection errors and stop execution if something is wrong
  320.                 if ($cpw_ldap_search_err{
  321.                     @ldap_unbind($cpw_ldap_con);
  322.                     // errors are added to msgs by cpw_ldap_uid_search()
  323.                     return $msgs;
  324.                 }
  325.  
  326.                 // we should check user password here.
  327.                 // suppress errors and check value returned by function call
  328.                 $cpw_ldap_cur_pass_array=@ldap_get_values($cpw_ldap_con,
  329.                                                          ldap_first_entry($cpw_ldap_con,$cpw_ldap_res2),'userpassword');
  330.  
  331.                 // check if ldap_get_values() have found userpassword field
  332.                 if ($cpw_ldap_cur_pass_array{
  333.                     array_push($msgs,_("Unable to find user's password attribute."));
  334.                     return $msgs;
  335.                 }
  336.  
  337.                 // compare passwords
  338.                 if (cpw_ldap_compare_pass($cpw_ldap_cur_pass_array[0],$curpw,$msgs)) {
  339.                     @ldap_unbind($cpw_ldap_con);
  340.                     // errors are added to $msgs by cpw_ldap_compare_pass()
  341.                     return $msgs;
  342.                 }
  343.             }
  344.         else {
  345.             $cpw_ldap_binding=@ldap_bind($cpw_ldap_con,$cpw_ldap_userdn,$curpw);
  346.         }
  347.  
  348.         if ($cpw_ldap_binding{
  349.             array_push($msgs,
  350.                        _("Unable to rebind to LDAP server."),
  351.                        sprintf(_("Server replied: %s"),ldap_error($cpw_ldap_con)));
  352.             @ldap_unbind($cpw_ldap_con);
  353.             return $msgs;
  354.         }
  355.  
  356.         // repeat search in order to get password info
  357.         if (isset($cpw_ldap_res2))
  358.             $cpw_ldap_search_err=cpw_ldap_uid_search($cpw_ldap_con,$cpw_ldap_basedn,$msgs,$cpw_ldap_res2,$cpw_ldap_userdn);
  359.  
  360.         // check for connection errors and stop execution if something is wrong
  361.         if ($cpw_ldap_search_err{
  362.             @ldap_unbind($cpw_ldap_con);
  363.             return $msgs;
  364.         }
  365.  
  366.         // getpassword. suppress errors and check value returned by function call
  367.         $cpw_ldap_cur_pass_array=@ldap_get_values($cpw_ldap_con,ldap_first_entry($cpw_ldap_con,$cpw_ldap_res2),'userpassword');
  368.  
  369.         // check if ldap_get_values() have found userpassword field.
  370.         // Error differs from previous one, because user managed to authenticate.
  371.         if ($cpw_ldap_cur_pass_array{
  372.             array_push($msgs,_("LDAP server uses different attribute to store user's password."));
  373.             return $msgs;
  374.         }
  375.  
  376.         // encrypt new password (old password is needed for plaintext encryption detection)
  377.         $cpw_ldap_new_pass=cpw_ldap_encrypt_pass($newpw,$cpw_ldap_cur_pass_array[0],$msgs,$curpw);
  378.  
  379.         if ($cpw_ldap_new_pass{
  380.             @ldap_unbind($cpw_ldap_con);
  381.             return $msgs;
  382.         }
  383.  
  384.         // set new password. suppress ldap_modify errors. script checks and displays ldap_modify errors.
  385.         $ldap_pass_change=@ldap_modify($cpw_ldap_con,$cpw_ldap_userdn,array('userpassword'=>$cpw_ldap_new_pass));
  386.  
  387.         // check if ldap_modify was successful
  388.         if($ldap_pass_change{
  389.             array_push($msgs,ldap_error($cpw_ldap_con));
  390.         }
  391.  
  392.         // close connection
  393.         @ldap_unbind($cpw_ldap_con);
  394.     else {
  395.         array_push($msgs,_("Unable to connect to LDAP server."));
  396.     }
  397.     return $msgs;
  398. }
  399.  
  400. /** backend support functions **/
  401.  
  402. /**
  403.  * Sanitizes LDAP query strings.
  404.  * original code - ldapquery plugin.
  405.  * See rfc2254
  406.  * @link http://www.faqs.org/rfcs/rfc2254.html
  407.  * @param string $string 
  408.  * @return string sanitized string
  409.  */
  410. function cpw_ldap_specialchars($string{
  411.     $sanitized=array('\\' => '\5c',
  412.                      '*' => '\2a',
  413.                      '(' => '\28',
  414.                      ')' => '\29',
  415.                      "\x00" => '\00');
  416.  
  417.     return str_replace(array_keys($sanitized),array_values($sanitized),$string);
  418. }
  419.  
  420. /**
  421.  * returns crypto algorithm used in password.
  422.  * @param string $pass encrypted/hashed password
  423.  * @return string lowercased crypto algorithm name
  424.  */
  425. function cpw_ldap_get_crypto($pass,$curpass=''{
  426.     $ret false;
  427.  
  428.     if (preg_match("/^\{(.+)\}+/",$pass,$crypto)) {
  429.         $ret=strtolower($crypto[1]);
  430.     }
  431.  
  432.     if ($ret=='crypt'{
  433.         // {CRYPT} can be standard des crypt, extended des crypt, md5 crypt or blowfish
  434.         // depends on first salt symbols (ext_des = '_', md5 = '$1$', blowfish = '$2')
  435.         // and length of salt (des = 2 chars, ext_des = 9, md5 = 12, blowfish = 16).
  436.         if (preg_match("/^\{crypt\}\\\$1\\\$+/i",$pass)) {
  437.             $ret='md5crypt';
  438.         elseif (preg_match("/^\{crypt\}\\\$2+/i",$pass)) {
  439.             $ret='blowfish';
  440.         elseif (preg_match("/^\{crypt\}_+/i",$pass)) {
  441.             $ret='extcrypt';
  442.         }
  443.     }
  444.  
  445.     // maybe password is plaintext
  446.     if ($ret && $curpass!='' && $pass==$curpass$ret='plaintext';
  447.  
  448.     return $ret;
  449. }
  450.  
  451. /**
  452.  * Search LDAP for user id.
  453.  * @param object $ldap_con ldap connection
  454.  * @param string $ldap_basedn ldap basedn
  455.  * @param array $msgs error messages
  456.  * @param object $results ldap search results
  457.  * @param string $userdn DN of found entry
  458.  * @param boolean $onlyone require unique search results
  459.  * @return boolean false if connection failed.
  460.  */
  461. function cpw_ldap_uid_search($ldap_con,$ldap_basedn,&$msgs,&$results,&$userdn,$onlyone=true{
  462.     global $cpw_ldap_userid_attr,$username;
  463.  
  464.     $ret=true;
  465.  
  466.     $results=ldap_search($ldap_con,$ldap_basedn,cpw_ldap_specialchars($cpw_ldap_userid_attr '=' $username));
  467.  
  468.     if ($results{
  469.         array_push($msgs,
  470.                    _("Unable to find user's DN."),
  471.                    _("Search error."),
  472.                    sprintf(_("Error: %s"),ldap_error($ldap_con)));
  473.         $ret=false;
  474.     elseif ($onlyone && ldap_count_entries($ldap_con,$results)>1{
  475.         array_push($msgs,_("Multiple userid matches found."));
  476.         $ret=false;
  477.     elseif ($userdn ldap_get_dn($ldap_con,ldap_first_entry($ldap_con,$results))) {
  478.         // ldap_get_dn() returned error
  479.         array_push($msgs,
  480.                    _("Unable to find user's DN."),
  481.                    _("ldap_get_dn error."));
  482.         $ret=false;
  483.     }
  484.     return $ret;
  485. }
  486.  
  487. /**
  488.  * Encrypts LDAP password
  489.  *
  490.  * if $cpw_ldap_default_crypto is set to empty string or $same_crypto is set,
  491.  * uses same crypto as in old password.
  492.  * See phpldapadmin password_hash() function
  493.  * @link http://phpldapadmin.sf.net
  494.  * @param string $pass string that has to be encrypted/hashed
  495.  * @param string $cur_pass_hash old password hash
  496.  * @param array $msgs error message
  497.  * @param string $curpass current password. Used for plaintext password detection.
  498.  * @return string encrypted/hashed password or false
  499.  */
  500. function cpw_ldap_encrypt_pass($pass,$cur_pass_hash,&$msgs,$curpass=''{
  501.     global $cpw_ldap_default_crypto;
  502.  
  503.     // which crypto should be used to encode/hash password
  504.     if ($cpw_ldap_default_crypto==''{
  505.         $ldap_crypto=cpw_ldap_get_crypto($cur_pass_hash,$curpass);
  506.     else {
  507.         $ldap_crypto=$cpw_ldap_default_crypto;
  508.     }
  509.     return cpw_ldap_password_hash($pass,$ldap_crypto,$msgs);
  510. }
  511.  
  512. /**
  513.  * create hashed password
  514.  * @param string $pass plain text password
  515.  * @param string $crypto used crypto algorithm
  516.  * @param array $msgs array used for error messages
  517.  * @param string $forced_salt salt that should be used during hashing.
  518.  *  Is used only when is not set to empty string. Salt should be formated
  519.  *  according to $crypto requirements.
  520.  * @return hashed password or false.
  521.  */
  522. function cpw_ldap_password_hash($pass,$crypto,&$msgs,$forced_salt=''{
  523.     // set default return code
  524.     $ret=false;
  525.  
  526.     // lowercase crypto just in case
  527.     $crypto=strtolower($crypto);
  528.  
  529.     // extra symbols used for random string in crypt salt
  530.     // squirrelmail GenerateRandomString() adds alphanumerics with third argument = 7.
  531.     $extra_salt_chars='./';
  532.  
  533.     // encrypt/hash password
  534.     switch ($crypto{
  535.     case 'md4':
  536.         // minimal requirement = php with mhash extension
  537.         if function_exists'mhash' && defined('MHASH_MD4')) {
  538.             $ret '{MD4}' base64_encodemhashMHASH_MD4$pass) );
  539.         else {
  540.             array_push($msgs,
  541. &n