Я разрабатываю простое приложение для «общения» с Amazon MWS API. Поскольку здесь задействовано много существующего кода, мне нужно сделать это в Delphi 2010 с компонентами Indy 10 (10.5.5), которые я успешно использовал для интеграции со многими другими API в прошлом. Однако API Amazon кажется невероятно чувствительным к мельчайшим деталям, вплоть до того, что все мои вызовы отклоняются с уже печально известным сообщением об ошибке «SignatureDoesNotMatch».
Вот что мне удалось на данный момент:
1) Мое приложение соберет запрос, подпишет его с помощью HMAC-SHA256 (с использованием библиотек OpenSSL) и отправит на конечную точку сервера Amazon.
2) Сама по себе подпись HMAC оказалась проблемой сама по себе, но теперь она работает правильно в 100% случаев (что подтверждено запросами, сгенерированными Amazon Scrachpad).
Однако, как я указывал ранее, мои запросы всегда отклоняются сервером MWS с ошибкой SignatureDoesNotMatch, даже если они достоверно верны. Единственное, что, на мой взгляд, может вызывать проблемы, - это способ, которым 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 в Scratchpad, а затем вставляю метку времени этого вызова в приведенный выше код, я получаю ТОЧНО ту же строку запроса здесь, с той же подписью и размером и т. Д. Но мой запрос Indy должен кодировать вещи по-другому, потому что серверу MWS звонок не нравится.
Я знаю, что MWS, по крайней мере, «читает» строку запроса, потому что, если я изменю метку времени на старую дату, вместо этого она вернет ошибку «срок действия запроса истек».
Техническая поддержка Amazon невежественна, отправляя сообщение каждый день с базовыми вещами вроде «Убедитесь, что секретный ключ правильный» (как будто получение подписи с HMAC-SHA256 и MD5 будет работать без действующего ключа !!!!).
Еще одна вещь: если я использую Wireshark для «просмотра» необработанного запроса как из приведенного выше кода, так и из образца кода C-Sharp Amazon, я тоже не заметлю разницы. Однако я не уверен, что Wireshark делает различие между UTF-8 и ASCII или любой другой кодировкой показываемого текста. Я все еще думаю, что это связано с плохой кодировкой UTC-8 или чем-то в этом роде.
Мы приветствуем и приветствуем идеи и предложения о том, как правильно кодировать вызов API, чтобы угодить богам Амазонки.