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

From: Date: Fri, 05 Sep 2003 08:07:15 +0000
Subject: Re: [PATCH] modification to Net_Sieve and Auth_SASL, looking for maintainer
References: 1  Groups: php.pear.dev 
Request: Send a blank email to pear-dev+get-21096@lists.php.net to get a copy of this message
It's a good idea to post this to bugs.php.net (put a link to the email on marc.theaims.. or news.php.net) = that way there is flagged as needing attention.. Regards Alan Etienne Goyer wrote:
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. ------------------------------------------------------------------------ --- 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 (#21096) next »