使用Delphi/Indy的Amazon MWS API调用
我正在开发一个与AmazonMWSAPI“对话”的简单应用程序。因为这里有很多现有的代码,所以我需要在Delphi2010中使用Indy10(10.5.5)组件来完成这项工作,我过去曾成功地将其与许多其他API集成。然而,amazonapi似乎对最小的细节非常敏感,以至于我的所有调用都被拒绝,并出现了臭名昭著的“signaturedesnotmatch”错误消息 以下是我迄今为止所取得的成就: 1) 我的应用程序将组装一个请求,使用HMAC-SHA256(使用OpenSSL库)对其进行签名,并将其发送到Amazon服务器端点 2) HMAC签名本身就是一个挑战,但它现在100%的时间都正常工作(通过Amazon Scrachpad生成的请求进行验证) 然而,正如我前面指出的,我的请求总是被MWS服务器以SignatureDesNotMatch错误拒绝,即使它们是可验证的正确的。我能想到的唯一可能导致问题的是Indy处理POST请求的方式,特别是文本编码过程 是否有人成功地将德尔福/印地客户机连接到MWS?如果是,使用了什么类型的HTTP设置?以下是我所拥有的:使用Delphi/Indy的Amazon MWS API调用,delphi,delphi-2010,indy,indy10,amazon-mws,Delphi,Delphi 2010,Indy,Indy10,Amazon Mws,我正在开发一个与AmazonMWSAPI“对话”的简单应用程序。因为这里有很多现有的代码,所以我需要在Delphi2010中使用Indy10(10.5.5)组件来完成这项工作,我过去曾成功地将其与许多其他API集成。然而,amazonapi似乎对最小的细节非常敏感,以至于我的所有调用都被拒绝,并出现了臭名昭著的“signaturedesnotmatch”错误消息 以下是我迄今为止所取得的成就: 1) 我的应用程序将组装一个请求,使用HMAC-SHA256(使用OpenSSL库)对其进行签名,并将
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;
如果我在Scratchpad中组装一个GetOrder调用,然后将该调用的时间戳粘贴到上面的代码中,我在这里得到完全相同的查询字符串,具有相同的签名和大小,等等。但是我的Indy请求必须以不同的方式编码,因为MWS服务器不喜欢该调用
我知道MWS至少在“读取”查询字符串,因为如果我将时间戳更改为旧日期,它将返回一个“RequestExpired”错误
亚马逊的技术支持毫无头绪,每天都会发布一条信息,上面写着“确保密钥正确”(就好像没有有效密钥就可以获得HMAC-SHA256和MD5签名一样!!!!)
还有一件事:如果我使用Wireshark“观察”来自上述代码和C-Sharp Amazon示例代码的原始请求,我也看不出有什么区别。但是,我不确定Wireshark是否区分UTF-8和ASCII,或者显示的文本是否有任何编码。我仍然认为这与糟糕的UTC-8编码或类似的东西有关
欢迎并赞赏关于如何正确编码API调用以取悦亚马逊诸神的想法和建议。发现了问题:Indy(以及Synapse)将端口号添加到“主机”标题行中,直到我用Fiddler更仔细地观察标题(谢谢,@Graymatter!!!!),我才意识到额外的一点
当我将端点更改为mws.amazonservices.com:443(而不仅仅是mws.amazonservices)时,我的签名的计算方式与AWS服务器的方式相同,并且一切工作都很完美。您是否尝试过使用类似fiddler的工具来询问从其他工具到服务的调用?然后,您可以将生成的请求与它们的请求进行比较。@Graymatter:我使用Wireshark监视流量,还将我的应用程序和Amazon库示例中的呼叫重定向到我自己的web服务器(以便比较请求)。在这两种情况下,请求和签名完全匹配。这就是为什么我认为这可能是一个编码问题。我建议fiddler的原因是,他们有许多不同的视图,包括十六进制,可以让你看到传递的原始数据。有趣。我也来看看这个选项。
TIdHTTP
仅当您在80以外的端口上请求HTTP url,或在443以外的端口上请求HTTPS url时,才会将端口添加到Host
头中。否则它将忽略端口,因为正在使用默认端口。@Remy:不确定这是否正确。我使用的是Indy 10.5.5,上面的示例甚至没有指定端口,但是添加了:443。Synapse库也会发生这种情况(当我不确定发生了什么时,我用它来排除故障)。但是,.NET或JavaScript调用(IE和Chrome)没有添加任何端口号。事实上,我只是创建了一个简单的测试用例,将一个TIdHTTP和一个TidsLioHandlerSocketOpenSSL放到表单中,然后向Amazon服务器发出GET请求。下面是捕获到的请求原始数据:GET HTTP/1.1 Host:mws.amazonservices.com:443 Accept:text/html,/Accept Encoding:identity User Agent:Mozilla/3.0(兼容;Indy Library)可能是某个地方的设置使我的Indy的行为与预期不同吗?主机:端口出现在GET
行本身意味着您必须通过代理,否则TIdHTTP
根本不会将主机:端口放在该行上。这就不同了。顺便说一句,您没有使用Indy的最新版本,即10.6.0.5137。从10.5.5开始,TIdHTTP的内部结构已经有了一些变化,现在已经有好几年了。我也考虑过代理,但事实是:用443对请求进行签名使其与Amazon自己的计算相匹配。如果没有端口号,签名将不匹配,因此远程主机也必须获得这样的主机头。是的,我知道我应该使用最新版本,所以也许这是一个很好的机会