[PATCH] modification to Net_Sieve and Auth_SASL, looking for maintainer
| From: | Etienne Goyer | 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 +?>
--- 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 +?>