[PATCH] modification to Net_Sieve and Auth_SASL, looking for maintainer

From: Date: Thu, 04 Sep 2003 19:32:40 +0000
Subject: [PATCH] modification to Net_Sieve and Auth_SASL, looking for maintainer
Groups: php.pear.dev 
Request: Send a blank email to pear-dev+get-21081@lists.php.net to get a copy of this message
Hi, I wrote on this list two weeks ago concerning a patch I was looking to get incoporated in Net_Sieve, and been advised to wait two more weeks. These two weks passed, so I am reposting my patch here in the hope that somebody could look at it. First, a short summary of the step I have taken so far. I wrote various improvement to the Net_Sieve PEAR module in early July. I sent my patch to Richard Heyes, who is listed as maintainer for the Net_Sieve module, for the first time the 21st July. Not receiving a response from M. Heyes, I investigated other mean of contacting him (another email address, maybe) and it came to my attention that M. Heyes would be unable to answer email for a few months. On another list, somebody suggested that I contact Damian Sosa who, while not listed as a maintainer for this package, was said to have taken maintainership of this module. I wrote to M. Sosa on the 12th August, but never received a response. In the meantime, I kept on working on Net_Sieve and added a new feature that would require in turn a modification to the Auth_SASL module. I notice that Auth_SASL is being maintained by both M. Heyes and Michael Bretterklieber. I tried to contact M. Bretterklieber, but he asked me to deal with M. Heyes instead since he was currently too busy to take care of my request. He told me that M. Heyes was now answering email. So I decided to wait for either M. Sosa or Heyes to answer my previous request before proceding further. Neither answered my email yet. So here I am, with three patch, looking for maintainers willing to lok at them. If either M. Heyes, Sosa or Bretterklieber are reading the list, maybe they can have a look at my patches. Otherwise, I wish somebody else here could take care of them and give me feedback on their suitability (and, hopefully, have them commited eventually). These modification are required for a project I am currently working on, so I am willing to put more work in them if necessary. Finally, here is a description of these patches. The patch to Net_Sieve include three major change : 1. Add so-called "proxy" authentication support, that is separation of authentication and authorization id. This make it possible to be authenticated under a set of credential, but act on behalf of another user if permitted. A few protocol support this concept, among them Managesieve. 2. Add DIGEST-MD5, CRAM-MD5 and LOGIN authentication support. Net_Sieve was currently only supporting PLAIN authentication. These change required modification to Auth_SASL; see below. 3. Add support for REFERRAL response from the server, and following said referral. This change is quite intrusive as it break the "send command, receive response" model that Net_Sieve was based on. From there on, sending of command and retrieval of response have to be done in one atomic operation to permit referral. AFAIK, these change does not break backward compatibility. Since the authcid replace the username and authzid is optionnal, authentication should continue to work. Also, no change had been made to public function. My changes to Auth_SASL are quite simple. They add authcid/authzid separation in the two mechanism that support them : PLAIN and DIGEST-MD5. While Net_Sieve did not require the change to Auth_SASL_Plain since PLAIN authentication is implemented in place (it's quite trivial, really), I still did it for the sake of uniformity. Again, backward compatibilty is guaranteed since authcid replace username and authzid is optionnal in the two module affected. I tested these patch to the best of my knowledge and ressource. They have been tested against the timsieved Managesieve server, configured to work in a Cyrus Murder setup. They could certainly use testing with other Managesieve server, if somebody have access to one. Your feedback are most welcome. I hope to have these patch commited as soon as possible and be over with them ! :) Thanks a lot, and sorry for the long post. -- Etienne Goyer Linux Québec Technologies Inc. http://www.LinuxQuebec.com etienne.goyer@linuxquebec.com

--- Sieve.php.orig Mon Jul 21 09:26:01 2003 +++ Sieve.php Wed Sep 3 16:58:03 2003 @@ -33,6 +33,7 @@ // +-----------------------------------------------------------------------+ require_once('Net/Socket.php'); +require_once('Auth/SASL.php'); /** * TODO @@ -104,20 +105,25 @@ * using the getError() method. * * @access public - * @param string $user Login username - * @param string $pass Login password + * @param string $authcid Authentication id (username) + * @param string $pass Password * @param string $host Hostname of server * @param string $port Port of server * @param string $logintype Type of login to perform + * @param string $authzid Authorization id */ - function Net_Sieve($user, $pass, $host = 'localhost', $port = 2000, $logintype = 'PLAIN') + function Net_Sieve($authcid, $pass, $host = 'localhost', $port = 2000, $logintype = '', $authzid = '') { $this->_state = NET_SIEVE_STATE_DISCONNECTED; - $this->_data['user'] = $user; - $this->_data['pass'] = $pass; - $this->_data['host'] = $host; - $this->_data['port'] = $port; + if (!isset($authzid)) { $authzid = $authcid; } + + $this->_data['authcid'] = $authcid; + $this->_data['pass'] = $pass; + $this->_data['host'] = $host; + $this->_data['port'] = $port; + $this->_data['logintype'] = $logintype; + $this->_data['authzid'] = $authzid; $this->_sock = &new Net_Socket(); if (PEAR::isError($res = $this->_connect($host, $port))) { @@ -125,7 +131,7 @@ return; } - if (PEAR::isError($res = $this->_login($user, $pass, $logintype))) { + if (PEAR::isError($res = $this->_login($authcid, $pass, $logintype, $authzid))) { $this->_error = $res; } } @@ -250,7 +256,7 @@ return $res; } // Get logon greeting/capability and parse - if(!PEAR::isError($res = $this->_getResponse())) { + if(!PEAR::isError($res = $this->_doCmd("CAPABILITY"))) { $this->_parseCapability($res); $this->_state = NET_SIEVE_STATE_AUTHORISATION; return true; @@ -266,25 +272,64 @@ * Logs into server. * * @access private - * @param string $user Login username - * @param string $pass Login password + * @param string $authcid Authentication id + * @param string $authzid Authorization id (if any) + * @param string $pass Password * @param string $logintype Type of login method to use * @return mixed True on success, PEAR_Error otherwise */ - function _login($user, $pass, $logintype = 'PLAIN') + function _login($authcid, $pass, $logintype = '', $authzid = '') { if (NET_SIEVE_STATE_AUTHORISATION == $this->_state) { - if ($logintype == 'PLAIN' AND in_array('PLAIN', $this->_capability['sasl'])) { - $this->_sendCmd(sprintf('AUTHENTICATE "PLAIN" "%s"', base64_encode(chr(0) . $user . chr(0) . $pass))); - - } elseif ($logintype == 'PLAIN' AND in_array('LOGIN', $this->_capability['sasl'])) { - $this->_sendCmd('AUTHENTICATE "LOGIN"'); - $this->_sendCmd(sprintf('"%s"', base64_encode($user))); - $this->_sendCmd(sprintf('"%s"', base64_encode($pass))); + if (($logintype == 'DIGEST-MD5' OR $logintype == '') AND + in_array('DIGEST-MD5', $this->_capability['sasl'])) { + + $this->_sendCmd(sprintf('AUTHENTICATE "DIGEST-MD5"')); + if (PEAR::isError($challenge = $this->_getChallenge())) { + return $challenge; + } + if (PEAR::isError($sasl = &Auth_SASL::factory('DigestMD5'))) { + return $sasl; + } + if (PEAR::isError($response = $sasl->getResponse($authcid, $pass, $challenge, $this->_data['host'], 'sieve', $authzid))) { + return $response; + } + $res = $this->_doCmd('"' . base64_encode($response) . '"'); + + } elseif (($logintype == 'CRAM-MD5' OR ($logintype == '' and $authzid == '')) AND + in_array('CRAM-MD5', $this->_capability['sasl'])) { + + $this->_sendCmd(sprintf('AUTHENTICATE "CRAM-MD5"')); + if (PEAR::isError($challenge = $this->_getChallenge())) { + return $challenge; + } + if (PEAR::isError($sasl = &Auth_SASL::factory('CramMD5'))) { + return $sasl; + } + if (PEAR::isError($response = $sasl->getResponse($authcid, $pass, $challenge))) { + return $response; + } + $res = $this->_doCmd('"' . base64_encode($response) . '"'); + + } elseif (($logintype == 'PLAIN' OR $logintype == '') AND + in_array('PLAIN', $this->_capability['sasl'])) { + + $res = $this->_doCmd(sprintf('AUTHENTICATE "PLAIN" "%s"', base64_encode($authzid . chr(0) . $authcid . chr(0) . $pass))); + + } elseif (($logintype == 'LOGIN' OR ($logintype == '' AND $authzid == '')) AND + in_array('LOGIN', $this->_capability['sasl'])) { + + $this->_sendCmd('AUTHENTICATE "LOGIN" "' . base64_encode($authcid) . '"'); + // Throw away password prompt + $this->_sock->readLine(); + $res = $this->_doCmd('"' . base64_encode($pass) . '"'); + + } else { + return PEAR::raiseError("No SASL mechanism found."); } - if (!PEAR::isError($res = $this->_getResponse())) { + if (!PEAR::isError($res)) { $this->_state = NET_SIEVE_STATE_TRANSACTION; return true; } else { @@ -305,9 +350,9 @@ function _cmdDeleteScript($scriptname) { if (NET_SIEVE_STATE_TRANSACTION === $this->_state) { - $this->_sendCmd(sprintf('DELETESCRIPT "%s"', $scriptname)); + $res = $this->_doCmd(sprintf('DELETESCRIPT "%s"', $scriptname)); - if (PEAR::isError($res = $this->_getResponse())) { + if (PEAR::isError($res)) { return $res; } else { return true; @@ -327,8 +372,8 @@ function _cmdGetScript($scriptname) { if (NET_SIEVE_STATE_TRANSACTION === $this->_state) { - $this->_sendCmd(sprintf('GETSCRIPT "%s"', $scriptname)); - if (PEAR::isError($res = $this->_getResponse())) { + $res = $this->_doCmd(sprintf('GETSCRIPT "%s"', $scriptname)); + if (PEAR::isError($res)) { return $res; } else { return preg_replace('/{[0-9]+}\r\n/', '', $res); @@ -349,9 +394,9 @@ function _cmdSetActive($scriptname) { if (NET_SIEVE_STATE_TRANSACTION === $this->_state) { - $this->_sendCmd(sprintf('SETACTIVE "%s"', $scriptname)); + $res = $this->_doCmd(sprintf('SETACTIVE "%s"', $scriptname)); - if (PEAR::isError($res = $this->_getResponse())) { + if (PEAR::isError($res)) { return $res; } else { $this->_activeScript = $scriptname; @@ -374,8 +419,8 @@ if (NET_SIEVE_STATE_TRANSACTION === $this->_state) { $scripts = array(); $activescript = null; - $this->_sendCmd('LISTSCRIPTS'); - if (PEAR::isError($res = $this->_getResponse())) { + $res = $this->_doCmd('LISTSCRIPTS'); + if (PEAR::isError($res)) { return $res; } else { $res = explode("\r\n", $res); @@ -406,9 +451,8 @@ function _cmdPutScript($scriptname, $scriptdata) { if (NET_SIEVE_STATE_TRANSACTION === $this->_state) { - $this->_sendCmd(sprintf('PUTSCRIPT "%s" {%d+}', $scriptname, strlen($scriptdata))); - $this->_sendCmd($scriptdata); - if (!PEAR::isError($res = $this->_getResponse())) { + $res = $this->_doCmd(sprintf("PUTSCRIPT \"%s\" {%d+}\r\n%s", $scriptname, strlen($scriptdata), $scriptdata)); + if (!PEAR::isError($res)) { return true; } else { return $res; @@ -428,13 +472,9 @@ { if (NET_SIEVE_STATE_DISCONNECTED !== $this->_state) { $this->_sendCmd('LOGOUT'); - if (!PEAR::isError($res = $this->_getResponse())) { - $this->_sock->disconnect(); - $this->_state = NET_SIEVE_STATE_DISCONNECTED; - return true; - } else { - return $res; - } + $this->_sock->disconnect(); + $this->_state = NET_SIEVE_STATE_DISCONNECTED; + return true; } else { return PEAR::raiseError('Not currently connected'); } @@ -449,8 +489,8 @@ function _cmdCapability() { if (NET_SIEVE_STATE_TRANSACTION === $this->_state) { - $this->_sendCmd('CAPABILITY'); - if (!PEAR::isError($res = $this->_getResponse())) { + $res = $this->_doCmd('CAPABILITY'); + if (!PEAR::isError($res)) { $this->_parseCapability($res); return true; } else { @@ -503,16 +543,46 @@ { $this->_sock->writeLine($cmd); } + + /** + * Retrieves the plaintext SASL challenge from the server. + * + * @access private + * @return mixed Reponse string + */ + function _getChallenge() + { + $challenge = ''; + $line = $this->_sock->readLine(); + + if ('no' == strtolower(substr($line, 0, 2)) or + 'bye' == strtolower(substr($line, 0, 3))) { + // SASL error + preg_match('/.*?\s(.*)/', $line, $matches); + return PEAR::raiseError($matches[1]); + } + + // Discard {nnn+} in literal + if (preg_match('/^\{\d*\+{0,1}\}\s*$/', $line)) { + $line = $this->_sock->readLine(); + } + preg_replace('/^\{\d*\+{0,1}\}\s*/', '', $line); + return base64_decode($line); + } + /** - * Retrieves a response from the server and, to a certain degree, - * parses it. + * Send a command and retrieves a response from the server. + * * * @access private + * @param string $cmd The command to send * @return mixed Reponse string if an OK response, PEAR_Error if a NO response */ - function _getResponse() + function _doCmd($cmd) { + $this->_sock->writeLine($cmd); + $response = ''; while (true) { @@ -520,16 +590,32 @@ if ('ok' == strtolower(substr($line, 0, 2))) { return rtrim($response); - } elseif ('no' == strtolower(substr($line, 0, 2))) { + } elseif ('no' == strtolower(substr($line, 0, 2))) { // Check for string literal error message if (preg_match('/^no {([0-9]+)\+?}/i', $line, $matches)) { $line .= str_replace("\r\n", ' ', $this->_sock->read($matches[1])); } return PEAR::raiseError(trim($response . substr($line, 2))); - } - + + } elseif ('bye' == strtolower(substr($line, 0, 3))) { + // Check for referral, then follow it. Otherwise, carp an error. + if (preg_match('/^bye \(referral "(.*?)"\)/i', $line, $matches)) { + // Follow referral + $this->_data['host'] = $matches[1]; + if (PEAR::isError($this->_cmdLogout()) or + PEAR::isError($this->_connect($this->_data['host'], $this->_data['port'])) or + PEAR::isError($this->_login($this->_data['authcid'], $this->_data['pass'], $this->_data['logintype'], $this->_data['authzid']))) { + return PEAR::raiseError("Can't follow referral to " . $this->_data['host']); + } + $this->_sock->writeLine($cmd); + } else { + return PEAR::raiseError(trim($response . substr($line, 3))); + } + } + $response .= $line . "\r\n"; } } + } -?> \ No newline at end of file +?> --- DigestMD5.php.orig Tue Aug 19 14:47:17 2003 +++ DigestMD5.php Tue Aug 26 16:56:14 2003 @@ -52,24 +52,29 @@ * requires a few extra parameters than the other * mechanisms, which are unavoidable. * - * @param string $user Username + * @param string $authcid Authentication id (username) * @param string $pass Password * @param string $challenge The digest challenge sent by the server * @param string $hostname The hostname of the machine you're connecting to * @param string $service The servicename (eg. imap, pop, acap etc) + * @param string $authzid Authorization id (username to proxy as) * @return string The digest response (NOT base64 encoded) * @access public */ - function getResponse($user, $pass, $challenge, $hostname, $service) + function getResponse($authcid, $pass, $challenge, $hostname, $service, $authzid = '') { $challenge = $this->_parseChallenge($challenge); + $authzid_string = ''; + if ($authzid != '') { + $authzid_string = ',authzid="' . $authzid . '"'; + } if (!empty($challenge)) { $cnonce = $this->_getCnonce(); $digest_uri = sprintf('%s/%s', $service, $hostname); - $response_value = $this->_getResponseValue($user, $pass, $challenge['realm'], $challenge['nonce'], $cnonce, $digest_uri); + $response_value = $this->_getResponseValue($authcid, $pass, $challenge['realm'], $challenge['nonce'], $cnonce, $digest_uri, $authzid); - return sprintf('username="%s",realm="%s",nonce="%s",cnonce="%s",nc="00000001",qop=auth,digest-uri="%s",response=%s,%d', $user, $challenge['realm'], $challenge['nonce'], $cnonce, $digest_uri, $response_value, $challenge['maxbuf']); + return sprintf('username="%s",realm="%s"' . $authzid_string . ',nonce="%s",cnonce="%s",nc="00000001",qop=auth,digest-uri="%s",response=%s,%d', $authcid, $challenge['realm'], $challenge['nonce'], $cnonce, $digest_uri, $response_value, $challenge['maxbuf']); } else { return PEAR::raiseError('Invalid digest challenge'); } @@ -140,18 +145,23 @@ /** * Creates the response= part of the digest response * - * @param string $user Username + * @param string $authcid Authentication id (username) * @param string $pass Password * @param string $realm Realm as provided by the server * @param string $nonce Nonce as provided by the server * @param string $cnonce Client nonce * @param string $digest_uri The digest-uri= value part of the response + * @param string $authzid Authorization id * @return string The response= part of the digest response * @access private */ - function _getResponseValue($user, $pass, $realm, $nonce, $cnonce, $digest_uri) + function _getResponseValue($authcid, $pass, $realm, $nonce, $cnonce, $digest_uri, $authzid = '') { - $A1 = sprintf('%s:%s:%s', pack('H32', md5(sprintf('%s:%s:%s', $user, $realm, $pass))), $nonce, $cnonce); + if ($authzid == '') { + $A1 = sprintf('%s:%s:%s', pack('H32', md5(sprintf('%s:%s:%s', $authcid, $realm, $pass))), $nonce, $cnonce); + } else { + $A1 = sprintf('%s:%s:%s:%s', pack('H32', md5(sprintf('%s:%s:%s', $authcid, $realm, $pass))), $nonce, $cnonce, $authzid); + } $A2 = 'AUTHENTICATE:' . $digest_uri; return md5(sprintf('%s:%s:00000001:%s:auth:%s', md5($A1), $nonce, $cnonce, md5($A2))); } @@ -181,4 +191,4 @@ } } } -?> \ No newline at end of file +?> --- Plain.php.orig Tue Aug 19 16:17:18 2003 +++ Plain.php Tue Aug 26 16:59:03 2003 @@ -50,13 +50,14 @@ /** * Returns PLAIN response * - * @param string $user Username - * @param string $pass Password - * @return string PLAIN Response + * @param string $authcid Authentication id (username) + * @param string $pass Password + * @param string $authzid Autorization id + * @return string PLAIN Response */ - function getResponse($user, $pass) + function getResponse($authcid, $pass, $authzid = '') { - return chr(0) . $user . chr(0) . $pass; + return $authzid . chr(0) . $authcid . chr(0) . $pass; } } -?> \ No newline at end of file +?>
« previous php.pear.dev (#21081) next »