Streaming API : track works; follow doesn't


#1

Hi everyone–

I’m having a bit of a conundrum here. When I do a track request, I get results. All is fine. But when I try to do a follow request, I get a 401 error. Thoughts?

public function start($_keywords = array(), $_user_ids = array())
{
   	if(!is_array($_keywords))
   	{
   		$_keywords = array($_keywords);
   	}
   	
   	if(!is_array($_user_ids))
   	{
   		$_user_ids = array($_user_ids);
   	}
   	
       while(1)
       {
           $fp = fsockopen("ssl://stream.twitter.com", 443, $errno, $errstr, 30);
           if (!$fp)
           {
               echo "ERROR: Twitter Stream Error: failed to open socket";
           } else
           {
               //
               // build the data and store it so we can get a length
               //
               $data = "";
               
               if(count($_keywords))
               {
               	$data .= 'track=' . rawurlencode(implode(",", $_keywords));
               }
               
               if(count($_user_ids))
               {
               	if(count($_keywords))
               	{
               		$data .= "&";
               	}
               	
               	$data .= 'follow=' . rawurlencode(implode(",", $_user_ids));
               }
               
               //$data .= "&count=20";
               
               echo "DATA: \n\n".$data."\n\n";
               
               //
               // store the current timestamp
               //
               $this->m_oauth_timestamp = time(); 

               //
               // generate the base string based on all the data
               //
               $base_string = 'POST&' . 
                   rawurlencode('https://stream.twitter.com/1.1/statuses/filter.json') . '&' .
                   rawurlencode('oauth_consumer_key=' . $this->m_oauth_consumer_key . '&' .
                       'oauth_nonce=' . $this->m_oauth_nonce . '&' .
                       'oauth_signature_method=' . $this->m_oauth_signature_method . '&' . 
                       'oauth_timestamp=' . $this->m_oauth_timestamp . '&' .
                       'oauth_token=' . $this->m_oauth_token . '&' .
                       'oauth_version=' . $this->m_oauth_version . '&' .
                       $data);

               echo "BASE STRING: \n\n".$base_string."\n\n"  ;      
                       
               //
               // generate the secret key to use to hash
               //
               $secret = rawurlencode($this->m_oauth_consumer_secret) . '&' . 
                   rawurlencode($this->m_oauth_token_secret);

               //
               // generate the signature using HMAC-SHA1
               //
               // hash_hmac() requires PHP >= 5.1.2 or PECL hash >= 1.1
               //
               $raw_hash = hash_hmac('sha1', $base_string, $secret, true);

               //
               // base64 then urlencode the raw hash
               //
               $this->m_oauth_signature = rawurlencode(base64_encode($raw_hash));

               //
               // build the OAuth Authorization header
               //
               $oauth = 'OAuth oauth_consumer_key="' . $this->m_oauth_consumer_key . '", ' .
                       'oauth_nonce="' . $this->m_oauth_nonce . '", ' .
                       'oauth_signature="' . $this->m_oauth_signature . '", ' .
                       'oauth_signature_method="' . $this->m_oauth_signature_method . '", ' .
                       'oauth_timestamp="' . $this->m_oauth_timestamp . '", ' .
                       'oauth_token="' . $this->m_oauth_token . '", ' .
                       'oauth_version="' . $this->m_oauth_version . '"';

               //
               // build the request
               //
               $request  = "POST /1.1/statuses/filter.json HTTP/1.1\r\n";
               $request .= "Host: stream.twitter.com\r\n";
               $request .= "Authorization: " . $oauth . "\r\n";
               $request .= "Content-Length: " . strlen($data) . "\r\n";
               $request .= "Content-Type: application/x-www-form-urlencoded\r\n\r\n";
               $request .= $data;

#2

Here is the response for a follow request:

REQUEST :

POST /1.1/statuses/filter.json HTTP/1.1
Host: stream.twitter.com
Authorization: OAuth oauth_consumer_key=“mykey”, oauth_nonce=“mynonce”, oauth_signature=“mysig”, oauth_signature_method=“HMAC-SHA1”, oauth_timestamp=“1370890521”, oauth_token=“mytoken”, oauth_version="1.0"
Content-Length: 15
Content-Type: application/x-www-form-urlencoded

follow=18647765

ERROR: HTTP/1.1 401 Unauthorized


#3

Signing parameters need to be sorted alphabetically.

Track starts with “t” which occurs after “o”, meaning the track parameter is inserted after the “oauth_*” parameters when building a base string, which is implicitly what you’re doing.

Follow starts with “f” which occurs before “o”, meaning that appending the follow parameter into the signature base string after the oauth parameters is incorrect.

I’d very much suggest using the tmhOAuth PHP library, as it will help avoid this and many other gotchas which come up when attempting to sign your own oauth requests by hand.


#4

Ahh, I see. Thank you so much!!!

I took a look at the tmh stuff, but the way you pass the callback in is kind of hack-y, among other things. It just doesn’t seem very clean.

Normally, I use Zend_Oauth for all my Oauth needs, but I don’t really understand how my usual method of making an Oauth call would integrate with process of keeping the stream open.


#5

Fair enough. For what it’s worth, tmhOAuth was written by a Twitter engineer who worked with streaming consumers for a while. It’s also pretty mature and he keeps it up to date with API changes. I’d also say it’s pretty difficult to write a good streaming client library which isn’t callback based, as the loop to read a streaming API response off of a socket contains a lot of boilerplate logic by necessity.