Вызов API Amazon MWS с использованием Delphi / Indy

Я разрабатываю простое приложение для «общения» с 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, чтобы угодить богам Амазонки.


person MiGz    schedule 09.05.2014    source источник
comment
Вы пробовали использовать что-то вроде fiddler для опроса вызовов из других инструментов в службу? Затем вы можете сравнить свои сгенерированные запросы с их запросами.   -  person Graymatter    schedule 10.05.2014
comment
@Graymatter: я использовал Wireshark для отслеживания трафика, а также перенаправлял вызовы из своего приложения и из образцов библиотеки Amazon на свой собственный веб-сервер (чтобы я мог сравнивать запросы). В обоих случаях запросы и подписи полностью совпадают. Вот почему я думаю, что это может быть проблема с кодировкой.   -  person MiGz    schedule 10.05.2014
comment
Причина, по которой я предложил fiddler, заключается в том, что у них есть несколько различных представлений, включая шестнадцатеричный, который позволит вам видеть передаваемые необработанные данные.   -  person Graymatter    schedule 10.05.2014
comment
Интересный. Я тоже посмотрю на этот вариант.   -  person MiGz    schedule 10.05.2014


Ответы (1)


Обнаружил проблему: Indy (и Synapse тоже) добавляет номер порта в строку заголовка "Host", и я не осознавал этот дополнительный бит, пока не посмотрел заголовки более внимательно с Fiddler (спасибо, @Graymatter !!!!).

Когда я меняю конечную точку на mws.amazonservices.com:443 (вместо просто mws.amazonservices), моя подпись рассчитывается так же, как и подпись сервера AWS, и все работает отлично.

person MiGz    schedule 10.05.2014
comment
TIdHTTP добавляет порт в заголовок Host, только если вы запрашиваете URL-адрес HTTP для порта, отличного от 80, или URL-адрес HTTPS для порта, отличного от 443. В противном случае он пропускает порт, поскольку используется порт по умолчанию. - person Remy Lebeau; 10.05.2014
comment
@ Реми: не уверен, что это правда. Я использую Indy 10.5.5, и в приведенном выше примере даже не указан порт, но: 443 был добавлен. Это также происходит с библиотекой Synapse (я использовал это для устранения неполадок, когда я не был уверен в том, что происходит). Однако при вызовах .NET или JavaScript (как в IE, так и в Chrome) номер порта не добавлялся. - person MiGz; 10.05.2014
comment
Фактически, я только что создал тестовый пример barebones, отбросив TIdHTTP и TIdSSLIOHandlerSocketOpenSSL в форму, а затем отправил запрос GET на сервер Amazon. Вот захваченные необработанные данные запроса: GET mws.amazonservices.com:443 HTTP / 1.1 Host : mws.amazonservices.com:443 Accept: text / html, / Accept-Encoding: identity User-Agent: Mozilla / 3.0 (совместимый; Indy Library) Может быть, это какая-то настройка, которая заставляет меня Инди ведет себя не так, как задумано? - person MiGz; 10.05.2014
comment
Тот факт, что Host: Port появляется в строке GET, означает, что вы должны проходить через прокси, иначе TIdHTTP вообще не поместит Host: Port в эту строку. Это имеет значение. Кстати, вы не используете последнюю версию Indy, а именно 10.6.0.5137. Во внутреннем устройстве TIdHTTP произошли изменения с 10.5.5, которому уже несколько лет. - person Remy Lebeau; 10.05.2014
comment
Я тоже думал о прокси, но факт в том, что подписание запроса С 443 позволяет ему соответствовать собственному расчету Amazon. Без номера порта подписи не будут совпадать, поэтому удаленный хост также должен получать заголовок Host, подобный этому. И да, я знаю, что мне следует использовать самую новую версию, так что, возможно, это отличный шанс сделать это. :-) - person MiGz; 11.05.2014