2014-05-09 20 views
2

我正在開發一個簡單的應用程序來與亞馬遜MWS API「交談」。由於很多現有的代碼都在這裏發揮作用,我需要在Delphi 2010中使用Indy 10(10.5.5)組件完成此項工作,而這些組件在過去已成功與其他許多API集成。但是,亞馬遜API似乎對最小的細節非常敏感,以至於我的所有電話都被拒絕,並且已經出現了臭名昭着的「SignatureDoesNotMatch」錯誤消息。使用Delphi/Indy的亞馬遜MWS API調用

這裏是我迄今完成:

1)我的應用程序將彙編的請求,與HMAC-SHA256簽名(使用OpenSSL庫),並將其發送到亞馬遜的服務器端點。

2)單獨證明HMAC簽名本身就是一項挑戰,但它現在可以在100%的時間內正常工作(如通過Amazon Scrachpad生成的請求進行驗證)。

但是,正如我前面指出的那樣,我的請求總是被具有SignatureDoesNotMatch錯誤的MWS服務器拒絕,即使它們是可驗證的正確的。我能想到的唯一可能導致問題的方式是Indy可能處理POST請求的方式,特別是文本編碼過程。

有沒有人成功地將Delphi/Indy客戶端連接到MWS?如果是這樣,那麼使用什麼樣的TIdHTTP設置?下面是我有:

procedure TAmazon.TestGetOrder(OrderID:String); 

const AwsAccessKey = 'MyAccessKey'; 
     AwsSecretKey = 'MySecretKey'; 
     MerchantID = 'MyMerchantID'; 
     MarketplaceID = 'MyMarketplaceID'; 
     ApiVersion = '2013-09-01'; 
     CallUri = '/Orders/2013-09-01'; 

var HTTP:TIdHTTP; 
    SSL:TIdSSLIOHandlerSocketOpenSSL; 
    SS:TStringStream; 
    Params:TStringList; 
    S,Timestamp,QueryString,Key,Value:String; 
    i:Integer; 

begin 
    HTTP:=TIdHTTP.Create(nil); 
    SSL:=TIdSSLIOHandlerSocketOpenSSL.Create(nil); 
    Params:=TStringList.Create; 
    try 
     Params.Delimiter:='&'; 
     Params.StrictDelimiter:=True; 

     // HTTP Client Options 
     HTTP.HTTPOptions:=HTTP.HTTPOptions+[hoKeepOrigProtocol]-[hoForceEncodeParams]; 
     HTTP.ConnectTimeout:=5000; 
     HTTP.ReadTimeout:=20000; 
     HTTP.ProtocolVersion:=pv1_1; 
     HTTP.IOHandler:=SSL; 
     HTTP.HandleRedirects:=True; 
     HTTP.Request.Accept:='text/plain, */*'; 
     HTTP.Request.AcceptLanguage:='en-US'; 
     HTTP.Request.ContentType:='application/x-www-form-urlencoded'; 
     HTTP.Request.CharSet:='utf-8'; 
     HTTP.Request.UserAgent:='MyApp/1.0 (Language=Delphi)'; 
     HTTP.Request.CustomHeaders.AddValue('x-amazon-user-agent',HTTP.Request.UserAgent); 

     // generate the timestamp per Amazon specs 
     Timestamp:=TIso8601.UtcDateTimeToIso8601(TIso8601.ToUtc(Now)); 
     // we can change the timestamp to match a value from the Scratchpad as a way to validate the signature: 
     //Timestamp:='2014-05-09T20:32:28Z'; 

     // add required parameters from API function GetOrder 
     Params.Add('Action=GetOrder'); 
     Params.Add('SellerId='+MerchantID); 
     Params.Add('AWSAccessKeyId='+AwsAccessKey); 
     Params.Add('Timestamp='+Timestamp); 
     Params.Add('Version='+ApiVersion); 
     Params.Add('SignatureVersion=2'); 
     Params.Add('SignatureMethod=HmacSHA256'); 
     Params.Add('AmazonOrderId.Id.1='+OrderID); 
     // generate the signature using the parameters above 
     Params.Add('Signature='+GetSignature(Params.Text,CallUri)); 

     // after generating the signature, make sure all values are properly URL-Encoded 
     for i:=0 to Params.Count-1 do begin 
     Key:=Params.Names[i]; 
     Value:=ParamEnc(Params.ValueFromIndex[i]); 
     QueryString:=QueryString+Key+'='+Value+'&'; 
     end; 
     Delete(QueryString,Length(QueryString),1); 

     // there are two ways to make the call... 
     // #1: according to the documentation, all parameters are supposed to be in 
     // the URL, and the body stream is supposed to be empty 
     SS:=TStringStream.Create; 
     try 
     try 
      Log('POST '+CallUri+'?'+QueryString); 
      S:=HTTP.Post('https://mws.amazonservices.com'+CallUri+'?'+QueryString,SS); 
     except 
      on E1:EIdHTTPProtocolException do begin 
       Log('RawHeaders='+#$D#$A+HTTP.Request.RawHeaders.Text); 
       Log('Protocol Exception:'+#$D#$A+StringReplace(E1.ErrorMessage,#10,#$D#$A,[rfReplaceAll])); 
      end; 
      on E2:Exception do 
       Log('Unknown Exception: '+E2.Message); 
     end; 
     Log('ResponseText='+S); 
     finally 
     SS.Free; 
     end; 

     // #2: both the Scratchpad and the CSharp client sample provided by Amazon 
     // do things in a different way, though... they POST the parameters in the 
     // body of the call, not in the query string 
     SS:=TStringStream.Create(QueryString,TEncoding.UTF8); 
     try 
     try 
      SS.Seek(0,0); 
      Log('POST '+CallUri+' (parameters in body/stream)'); 
      S:=HTTP.Post('https://mws.amazonservices.com'+CallUri,SS); 
     except 
      on E1:EIdHTTPProtocolException do begin 
       Log('RawHeaders='+#$D#$A+HTTP.Request.RawHeaders.Text); 
       Log('Protocol Exception:'+#$D#$A+StringReplace(E1.ErrorMessage,#10,#$D#$A,[rfReplaceAll])); 
      end; 
      on E2:Exception do 
       Log('Unknown Exception: '+E2.Message); 
     end; 
     Log('ResponseText='+S); 
     finally 
     SS.Free; 
     end; 
    finally 
     Params.Free; 
     SSL.Free; 
     HTTP.Free; 
    end; 
end; 

如果我組裝在便籤一個GetOrder呼叫,那麼該呼叫的時間戳粘貼到上面的代碼,我得到完全相同的查詢字符串這裏,具有相同簽名和大小等但是我的Indy請求必須以不同的方式進行編碼,因爲MWS服務器不喜歡這個呼叫。

我知道MWS至少「讀取」查詢字符串,因爲如果我將時間戳更改爲舊日期,它將返回「請求過期」錯誤。

亞馬遜的技術支持無能爲力,每天都會發布一條消息,內容是「確保密鑰正確」等基本信息(就像使用HMAC-SHA256和MD5獲得簽名無需有效密鑰!!!! )。還有一件事:如果我使用Wireshark從上面的代碼和C-Sharp亞馬遜示例代碼中「觀察」原始請求,我也無法區分。但是,我不確定Wireshark是否區分UTF-8和ASCII或者顯示的文本是什麼編碼。我仍然認爲它與壞的UTC-8編碼或類似的東西有關。

有關如何正確編碼API調用以取悅亞馬遜神的想法和建議,歡迎和讚賞。

+0

您是否嘗試過使用類似提琴手的方式來詢問其他工具對服務的調用?然後,您可以將生成的請求與其中的請求進行比較 – Graymatter

+0

@Graymatter:我使用Wireshark來觀察流量,並且還將來自應用和亞馬遜庫樣本的調用重定向到我自己的Web服務器(這樣我就可以比較這些請求)。在這兩種情況下,請求和簽名完全匹配。這就是爲什麼我認爲這可能是一個編碼問題。 – user1595471

+0

我建議小提琴手的原因是他們有許多不同的視圖,包括十六進制,它可以讓你看到傳遞的原始數據。 – Graymatter

回答

2

發現問題:Indy(和Synapse也一樣)將端口號添加到「Host」標題行,並且我沒有意識到額外的位,直到我更加仔細地觀看了Fiddler的標題(謝謝,@Graymatter! !!)。

當我將端點更改爲mws.amazonservices.com:443(而不僅僅是mws.amazonservices)時,那麼我的簽名與AWS服務器的計算方式相同,並且一切正常。

+0

只有當您請求80以外的端口上的HTTP URL或443以外的端口上的HTTPS URL時,'TIdHTTP'纔會將端口添加到'Host'標頭。否則,由於正在使用默認端口,因此它會省略端口。 –

+0

@雷米:不確定這是否準確。我使用Indy 10.5.5,上面的例子甚至沒有指定端口,但是:添加了443。這也發生在Synapse圖書館(當我不確定發生了什麼時,我用它來排除故障)。但是,沒有端口號被添加.NET或JavaScript調用(包括IE和Chrome)。 – user1595471

+0

實際上,我通過將TIdHTTP和TIdSSLIOHandlerSocketOpenSSL放入表單並創建了一個準系統測試用例,然後向Amazon服務器發出GET請求。這裏的請求已捕獲原始數據: GET https://mws.amazonservices.com:443/ HTTP/1.1 主持人:mws.amazonservices.com:443 接受:text/html的,*/* 接受 - 編碼:身份 用戶代理:Mozilla/3.0(兼容; Indy庫) 它可能是一個設置的地方,使我的Indy的行爲不同於預期的? – user1595471