Browse Source

ZF-7033 - Fix Zend_Service_Amazon authentication violation

git-svn-id: http://framework.zend.com/svn/framework/standard/trunk@17144 44c647ce-9c0f-0410-b52a-842ac1e357ba
beberlei 16 years ago
parent
commit
5e410a5f1c

+ 14 - 12
documentation/manual/en/module_specs/Zend_Service_Amazon.xml

@@ -41,15 +41,17 @@
             </itemizedlist>
         </para>
         <para>
-            In order to use <classname>Zend_Service_Amazon</classname>, you should already have an Amazon developer API key. To
-            get a key and for more information, please visit the
-            <ulink url="http://www.amazon.com/gp/aws/landing.html">Amazon Web Services</ulink> web site.
+            In order to use <classname>Zend_Service_Amazon</classname>, you should already have an Amazon developer API key
+            aswell as a secret key. To get a key and for more information, please visit the
+            <ulink url="http://aws.amazon.com/">Amazon Web Services</ulink> web site. As of August 15th, 2009 you can
+            only use the Amazon Product Advertising API through <classname>Zend_Service_Amazon</classname>, when specifying
+            the additional secret key.
         </para>
         <note>
             <title>Attention</title>
             <para>
-                Your Amazon developer API key is linked to your Amazon identity,
-                so take appropriate measures to keep your API key private.
+                Your Amazon developer API and secret keys are linked to your Amazon identity,
+                so take appropriate measures to keep them private.
             </para>
         </note>
         <example id="zend.service.amazon.introduction.example.itemsearch">
@@ -58,7 +60,7 @@
                 In this example, we search for PHP books at Amazon and loop through the results, printing them.
             </para>
             <programlisting language="php"><![CDATA[
-$amazon = new Zend_Service_Amazon('AMAZON_API_KEY');
+$amazon = new Zend_Service_Amazon('AMAZON_API_KEY', 'US', 'AMAZON_SECRET_KEY');
 $results = $amazon->itemSearch(array('SearchIndex' => 'Books',
                                      'Keywords' => 'php'));
 foreach ($results as $result) {
@@ -73,7 +75,7 @@ foreach ($results as $result) {
                 resembles the Fluent Interface design pattern.
             </para>
             <programlisting language="php"><![CDATA[
-$query = new Zend_Service_Amazon_Query('AMAZON_API_KEY');
+$query = new Zend_Service_Amazon_Query('AMAZON_API_KEY', 'US, 'AMAZON_SECRET_KEY');
 $query->category('Books')->Keywords('PHP');
 $results = $query->search();
 foreach ($results as $result) {
@@ -93,7 +95,7 @@ foreach ($results as $result) {
             <title>Choosing an Amazon Web Service Country</title>
             <programlisting language="php"><![CDATA[
 // Connect to Amazon in Japan
-$amazon = new Zend_Service_Amazon('AMAZON_API_KEY', 'JP');
+$amazon = new Zend_Service_Amazon('AMAZON_API_KEY', 'JP', 'AMAZON_SECRET_KEY');
 ]]></programlisting>
         </example>
         <note>
@@ -113,7 +115,7 @@ $amazon = new Zend_Service_Amazon('AMAZON_API_KEY', 'JP');
         <example id="zend.service.amazon.itemlookup.example.asin">
             <title>Looking up a Specific Amazon Item by ASIN</title>
             <programlisting language="php"><![CDATA[
-$amazon = new Zend_Service_Amazon('AMAZON_API_KEY');
+$amazon = new Zend_Service_Amazon('AMAZON_API_KEY', 'US', 'AMAZON_SECRET_KEY');
 $item = $amazon->itemLookup('B0000A432X');
 ]]></programlisting>
         </example>
@@ -140,7 +142,7 @@ $item = $amazon->itemLookup('B0000A432X');
         <example id="zend.service.amazon.itemsearch.example.basic">
             <title>Performing Amazon Item Searches</title>
             <programlisting language="php"><![CDATA[
-$amazon = new Zend_Service_Amazon('AMAZON_API_KEY');
+$amazon = new Zend_Service_Amazon('AMAZON_API_KEY', 'US', 'AMAZON_SECRET_KEY');
 $results = $amazon->itemSearch(array('SearchIndex' => 'Books',
                                      'Keywords' => 'php'));
 foreach ($results as $result) {
@@ -155,7 +157,7 @@ foreach ($results as $result) {
                 in the response.
             </para>
             <programlisting language="php"><![CDATA[
-$amazon = new Zend_Service_Amazon('AMAZON_API_KEY');
+$amazon = new Zend_Service_Amazon('AMAZON_API_KEY', 'US', 'AMAZON_SECRET_KEY');
 $results = $amazon->itemSearch(array(
     'SearchIndex'   => 'Books',
     'Keywords'      => 'php',
@@ -201,7 +203,7 @@ foreach ($results as $result) {
                     respective values:
                 </para>
                 <programlisting language="php"><![CDATA[
-$query = new Zend_Service_Amazon_Query('MY_API_KEY');
+$query = new Zend_Service_Amazon_Query('MY_API_KEY', 'US', 'AMAZON_SECRET_KEY');
 $query->Category('Books')->Keywords('PHP');
 $results = $query->search();
 foreach ($results as $result) {

+ 96 - 16
library/Zend/Service/Amazon.php

@@ -21,6 +21,10 @@
  * @version    $Id$
  */
 
+/**
+ * @see Zend_Rest_Client
+ */
+require_once 'Zend/Rest/Client.php';
 
 /**
  * @category   Zend
@@ -39,6 +43,16 @@ class Zend_Service_Amazon
     public $appId;
 
     /**
+     * @var string
+     */ 
+    protected $_secretKey = null;
+
+    /**
+     * @var string
+     */
+    protected $_baseUri = null;
+
+    /**
      * List of Amazon Web Service base URLs, indexed by country code
      *
      * @var array
@@ -55,7 +69,7 @@ class Zend_Service_Amazon
      *
      * @var Zend_Rest_Client
      */
-    protected $_rest;
+    protected $_rest = null;
 
 
     /**
@@ -66,11 +80,12 @@ class Zend_Service_Amazon
      * @throws Zend_Service_Exception
      * @return Zend_Service_Amazon
      */
-    public function __construct($appId, $countryCode = 'US')
+    public function __construct($appId, $countryCode = 'US', $secretKey = null)
     {
         $this->appId = (string) $appId;
-        $countryCode = (string) $countryCode;
+        $this->_secretKey = $secretKey;
 
+        $countryCode = (string) $countryCode;
         if (!isset($this->_baseUriList[$countryCode])) {
             /**
              * @see Zend_Service_Exception
@@ -79,11 +94,7 @@ class Zend_Service_Amazon
             throw new Zend_Service_Exception("Unknown country code: $countryCode");
         }
 
-        /**
-         * @see Zend_Rest_Client
-         */
-        require_once 'Zend/Rest/Client.php';
-        $this->_rest = new Zend_Rest_Client($this->_baseUriList[$countryCode]);
+        $this->_baseUri = $this->_baseUriList[$countryCode];
     }
 
 
@@ -97,10 +108,13 @@ class Zend_Service_Amazon
      */
     public function itemSearch(array $options)
     {
+        $client = $this->getRestClient();
+        $client->setUri($this->_baseUri);
+
         $defaultOptions = array('ResponseGroup' => 'Small');
         $options = $this->_prepareOptions('ItemSearch', $options, $defaultOptions);
-        $this->_rest->getHttpClient()->resetParameters();
-        $response = $this->_rest->restGet('/onca/xml', $options);
+        $client->getHttpClient()->resetParameters();
+        $response = $client->restGet('/onca/xml', $options);
 
         if ($response->isError()) {
             /**
@@ -134,19 +148,23 @@ class Zend_Service_Amazon
      */
     public function itemLookup($asin, array $options = array())
     {
-        $defaultOptions = array('IdType' => 'ASIN', 'ResponseGroup' => 'Small');
+        $client = $this->getRestClient();
+        $client->setUri($this->_baseUri);
+        $client->getHttpClient()->resetParameters();
+
+        $defaultOptions = array('ResponseGroup' => 'Small');
         $options['ItemId'] = (string) $asin;
         $options = $this->_prepareOptions('ItemLookup', $options, $defaultOptions);
-        $this->_rest->getHttpClient()->resetParameters();
-        $response = $this->_rest->restGet('/onca/xml', $options);
+        $response = $client->restGet('/onca/xml', $options);
 
         if ($response->isError()) {
             /**
              * @see Zend_Service_Exception
              */
             require_once 'Zend/Service/Exception.php';
-            throw new Zend_Service_Exception('An error occurred sending request. Status code: '
-                                           . $response->getStatus());
+            throw new Zend_Service_Exception(
+                'An error occurred sending request. Status code: ' . $response->getStatus()
+            );
         }
 
         $dom = new DOMDocument();
@@ -179,9 +197,24 @@ class Zend_Service_Amazon
      */
     public function getRestClient()
     {
+        if($this->_rest === null) {
+            $this->_rest = new Zend_Rest_Client();
+        }
         return $this->_rest;
     }
 
+    /**
+     * Set REST client
+     *
+     * @param Zend_Rest_Client
+     * @return Zend_Service_Amazon
+     */
+    public function setRestClient(Zend_Rest_Client $client)
+    {
+        $this->_rest = $client;
+        return $this;
+    }
+
 
     /**
      * Prepare options for request
@@ -193,9 +226,10 @@ class Zend_Service_Amazon
      */
     protected function _prepareOptions($query, array $options, array $defaultOptions)
     {
-        $options['SubscriptionId'] = $this->appId;
+        $options['AWSAccessKeyId'] = $this->appId;
         $options['Service']        = 'AWSECommerceService';
         $options['Operation']      = (string) $query;
+        $options['Version']        = '2005-10-05';
 
         // de-canonicalize out sort key
         if (isset($options['ResponseGroup'])) {
@@ -208,9 +242,55 @@ class Zend_Service_Amazon
         }
 
         $options = array_merge($defaultOptions, $options);
+
+        if($this->_secretKey !== null) {
+            $options['Timestamp'] = gmdate("Y-m-d\TH:i:s\Z");;
+            ksort($options);
+            $options['Signature'] = self::computeSignature($this->_baseUri, $this->_secretKey, $options);
+        }
+
         return $options;
     }
 
+    /**
+     * Compute Signature for Authentication with Amazon Product Advertising Webservices
+     *
+     * @param  string $baseUri
+     * @param  string $secretKey
+     * @param  array $options
+     * @return string
+     */
+    static public function computeSignature($baseUri, $secretKey, array $options)
+    {
+        require_once "Zend/Crypt/Hmac.php";
+
+        $signature = self::buildRawSignature($baseUri, $options);
+        return base64_encode(
+            Zend_Crypt_Hmac::compute($secretKey, 'sha256', $signature, Zend_Crypt_Hmac::BINARY)
+        );
+    }
+
+    /**
+     * Build the Raw Signature Text
+     *
+     * @param  string $baseUri
+     * @param  array $options
+     * @return string
+     */
+    static public function buildRawSignature($baseUri, $options)
+    {
+        ksort($options);
+        $params = array();
+        foreach($options AS $k => $v) {
+            $params[] = $k."=".rawurlencode($v);
+        }
+
+        return sprintf("GET\n%s\n/onca/xml\n%s",
+            str_replace('http://', '', $baseUri),
+            implode("&", $params)
+        );
+    }
+
 
     /**
      * Check result for errors

+ 71 - 0
tests/Zend/Service/Amazon/OfflineTest.php

@@ -145,4 +145,75 @@ class Zend_Service_Amazon_OfflineTest extends PHPUnit_Framework_TestCase
 
         $similarproduct = new Zend_Service_Amazon_SimilarProduct($product);
     }
+
+    public function dataSignatureEncryption()
+    {
+        return array(
+            array(
+                'http://webservices.amazon.com',
+                array(
+                    'Service' => 'AWSECommerceService',
+                    'AWSAccessKeyId' => '00000000000000000000',
+                    'Operation' => 'ItemLookup',
+                    'ItemId' => '0679722769',
+                    'ResponseGroup' => 'ItemAttributes,Offers,Images,Reviews',
+                    'Version' => '2009-01-06',
+                    'Timestamp' => '2009-01-01T12:00:00Z',
+                ),
+                "GET\n".
+                "webservices.amazon.com\n".
+                "/onca/xml\n".
+                "AWSAccessKeyId=00000000000000000000&ItemId=0679722769&Operation=I".
+                "temLookup&ResponseGroup=ItemAttributes%2COffers%2CImages%2CReview".
+                "s&Service=AWSECommerceService&Timestamp=2009-01-01T12%3A00%3A00Z&".
+                "Version=2009-01-06",
+                'Nace%2BU3Az4OhN7tISqgs1vdLBHBEijWcBeCqL5xN9xg%3D'
+            ),
+            array(
+                'http://ecs.amazonaws.co.uk',
+                array(
+                    'Service' => 'AWSECommerceService',
+                    'AWSAccessKeyId' => '00000000000000000000',
+                    'Operation' => 'ItemSearch',
+                    'Actor' => 'Johnny Depp',
+                    'ResponseGroup' => 'ItemAttributes,Offers,Images,Reviews,Variations',
+                    'Version' => '2009-01-01',
+                    'SearchIndex' => 'DVD',
+                    'Sort' => 'salesrank',
+                    'AssociateTag' => 'mytag-20',
+                    'Timestamp' => '2009-01-01T12:00:00Z',
+                ),
+                "GET\n".
+                "ecs.amazonaws.co.uk\n".
+                "/onca/xml\n".
+                "AWSAccessKeyId=00000000000000000000&Actor=Johnny%20Depp&Associate".
+                "Tag=mytag-20&Operation=ItemSearch&ResponseGroup=ItemAttributes%2C".
+                "Offers%2CImages%2CReviews%2CVariations&SearchIndex=DVD&Service=AW".
+                "SECommerceService&Sort=salesrank&Timestamp=2009-01-01T12%3A00%3A0".
+                "0Z&Version=2009-01-01",
+                'TuM6E5L9u%2FuNqOX09ET03BXVmHLVFfJIna5cxXuHxiU%3D',
+            ),
+        );
+    }
+
+    /**
+     * Checking if signature Encryption due on August 15th for Amazon Webservice API is working correctly.
+     *
+     * @dataProvider dataSignatureEncryption
+     * @group ZF-7033
+     */
+    public function testSignatureEncryption($baseUri, $params, $expectedStringToSign, $expectedSignature)
+    {
+        $this->assertEquals(
+            $expectedStringToSign,
+            Zend_Service_Amazon::buildRawSignature($baseUri, $params)
+        );
+
+        $this->assertEquals(
+            $expectedSignature,
+            rawurlencode(Zend_Service_Amazon::computeSignature(
+                $baseUri, '1234567890', $params
+            ))
+        );
+    }
 }

+ 15 - 3
tests/Zend/Service/Amazon/OnlineTest.php

@@ -80,9 +80,21 @@ class Zend_Service_Amazon_OnlineTest extends PHPUnit_Framework_TestCase
      */
     public function setUp()
     {
-        $this->_amazon = new Zend_Service_Amazon(constant('TESTS_ZEND_SERVICE_AMAZON_ONLINE_ACCESSKEYID'));
+        if(!defined('TESTS_ZEND_SERVICE_AMAZON_ONLINE_ACCESSKEYID') || !defined('TESTS_ZEND_SERVICE_AMAZON_ONLINE_SECRETKEY')) {
+            $this->markTestSkipped('Constants AccessKeyId and SecretKey have to be set.');
+        }
+
+        $this->_amazon = new Zend_Service_Amazon(
+            TESTS_ZEND_SERVICE_AMAZON_ONLINE_ACCESSKEYID,
+            'US',
+            TESTS_ZEND_SERVICE_AMAZON_ONLINE_SECRETKEY
+        );
 
-        $this->_query = new Zend_Service_Amazon_Query(constant('TESTS_ZEND_SERVICE_AMAZON_ONLINE_ACCESSKEYID'));
+        $this->_query = new Zend_Service_Amazon_Query(
+            TESTS_ZEND_SERVICE_AMAZON_ONLINE_ACCESSKEYID,
+            'US',
+            TESTS_ZEND_SERVICE_AMAZON_ONLINE_SECRETKEY
+        );
 
         $this->_httpClientAdapterSocket = new Zend_Http_Client_Adapter_Socket();
 
@@ -96,7 +108,7 @@ class Zend_Service_Amazon_OnlineTest extends PHPUnit_Framework_TestCase
 
     /**
      * Ensures that itemSearch() works as expected when searching for PHP books
-     *
+     * @group ItemSearchPhp
      * @return void
      */
     public function testItemSearchBooksPhp()