2016-10-17 2 views
3

Ich muss eine Signatur für Amazon MWS generieren und entschied mich, eine Lösung nur mit den Komponenten und Klassen zu finden, die mit Delphi kommen. Da ich Indy für den HTTP-Post selbst verwende, schien es eine gute Idee zu sein, Indy-Klassen für die Berechnung des RFC 2104-kompatiblen HMAC zu verwenden.Erstellen einer Amazon MWS-Signatur mit Delphi XE7 und Indy-Klassen

Für andere, die auf Amazon Integration annehmen, die Schaffung der „canonicalized Abfrage String“ ist sehr gut in den Amazon-Tutorial erklärt: http://docs.developer.amazonservices.com/en_DE/dev_guide/DG_ClientLibraries.html Seien Sie vorsichtig, gerade # 10 für Zeilenumbruch verwenden, wie # 13 # 10 oder # 13 wird mit einer falschen Signatur fehlschlagen. Es kann auch wichtig sein, "amazon Endpoint" (Host) ": 443" hinzuzufügen, abhängig von der TIdHttp-Version, wie in Frage Nr. 23573799 erklärt.

Um eine gültige Signatur zu erstellen, müssen wir einen HMAC mit SHA256 mit der Abfragezeichenfolge und dem SecretKey berechnen, den wir von Amazon nach der Registrierung erhalten haben, und dann muss das Ergebnis in BASE64 kodiert werden.

Die Abfragezeichenfolge wird ordnungsgemäß generiert und ist identisch mit der Zeichenfolge, die das Amazon Scratchpad erstellt. Aber der Anruf ist fehlgeschlagen, weil die Signatur nicht korrekt ist.

Nach einigen Tests stellte ich fest, dass die Signatur, die ich von meiner Abfragezeichenkette erhielt, nicht das selbe ist, das ich erhielt, als ich PHP verwendete, um es zu erzeugen. Das PHP-Ergebnis wird als korrekt angesehen, da meine PHP-Lösung seit langem mit Amazon funktioniert, das Delphi-Ergebnis ist anders, was nicht stimmt.

Um das Testen zu erleichtern, verwende ich '1234567890' als Wert für die Abfragezeichenfolge und 'ABCDEFG' als Ersatz für den SecretKey. Wenn das Ergebnis, das ich mit Delphi erhalte, dasselbe ist wie das Ergebnis, das ich mit PHP erhalte, sollte das Problem gelöst sein, glaube ich.

Hier ist, wie ich das richtige Ergebnis mit PHP erhalten:

echo base64_encode(hash_hmac('sha256', '1234567890', 'ABCDEFG', TRUE)); 

Dies zeigt ein Ergebnis der

aRGlc3RY1pKmKX0hvorkVKNcPigiJX2rksqXzlAeCLg= 

Der folgende Delphi XE7 Code gibt das falsche Ergebnis, während der indy-Version verwenden, kommt mit Delphi XE7:

uses 
  IdHash, IdHashSHA, IdHMACSHA1, IdSSLOpenSSL, IdGlobal, IdCoderMIME; 

function GenerateSignature(const AData, AKey: string): string; 
var 
   AHMAC: TIdBytes; 
begin 
     IdSSLOpenSSL.LoadOpenSSLLibrary; 

     With TIdHMACSHA256.Create do 
      try 
         Key:= ToBytes(AKey, IndyTextEncoding_UTF16LE); 
         AHMAC:= HashValue(ToBytes(AData, IndyTextEncoding_UTF16LE)); 
         Result:= TIdEncoderMIME.EncodeBytes(AHMAC); 
      finally 
         Free; 
      end; 
end; 

Hier das Ergebnis, das in einem Memo mit angezeigt wird

Memo.Lines.Text:= GenerateSignature('1234567890', 'ABCDEFG'); 

ist:

jg6Oddxvv57fFdcCPXrqGWB9YD5rSvtmGnZWL0X+y0Y= 

Ich glaube, das Problem etwas mit den Kodierungen zu tun hat, so habe ich einige der Forschung rund um das getan. Wie das Amazon Tutorial (Link siehe oben) sagt, erwartet Amazon eine utf8 Kodierung.

Da die Indy-Funktion "ToBytes" einen String erwartet, der in meiner Delphi-Version ein UnicodeString ist, beende ich das Testen mit anderen String-Typen als UTF8String für Parameter oder Variablen, aber ich weiß einfach nicht, wo utf8 in Position kommen soll . Ich weiß auch nicht, ob die Kodierungen, die ich im obigen Code verwende, die richtigen sind. Ich wähle UTF16LE, weil UnicodeString utf16-kodiert ist (Details siehe http://docwiki.embarcadero.com/RADStudio/Seattle/en/String_Types_(Delphi)) und LE (Little-Endian) wird am häufigsten auf modernen Rechnern verwendet. Auch die TEncodings von Delphi selbst gibt es "Unicode" und "BigEndianUnicode", so dass "Unicode" LE und eine Art "Standard" Unicode zu sein scheint. Natürlich habe ich getestet IndyTextEncoding_UTF8 anstelle von IndyTextEncoding_UTF16LE im obigen Code zu verwenden, aber es funktioniert sowieso nicht.

Da

TIdEncoderMIME.EncodeBytes(AHMAC); 

ist das TidBytes zu einem Strom zuerst zu schreiben und dann alles mit 8-Bit-Codierung zu lesen, könnte dies auch eine Quelle des Problems, so dass ich auch getestet mit

Result:= BytesToString(AHMAC, IndyTextEncoding_UTF16LE); 
Result:= TIdEncoderMIME.EncodeString(Result, IndyTextEncoding_UTF16LE); 

aber das Ergebnis ist das gleiche.

Wenn Sie das Haupt-Code zu sehen, die Anforderung für die Erstellung, hier ist es:

function TgboAmazon.MwsRequest(const AFolder, AVersion: string; 
    const AParams: TStringList; const AEndPoint: string): string; 
var 
    i: Integer; 
    SL: TStringList; 
    AMethod, AHost, AURI, ARequest, AStrToSign, APath, ASignature: string; 
    AKey, AValue, AQuery: string; 
    AHTTP: TIdHTTP; 
    AStream, AResultStream: TStringStream; 
begin 
    AMethod:= 'POST'; 
    AHost:= AEndPoint; 
    AURI:= '/' + AFolder + '/' + AVersion; 

    AQuery:= ''; 
    SL:= TStringList.Create; 
    try 
     SL.Assign(AParams); 
     SL.Values['AWSAccessKeyId']:= FAWSAccessKeyId; 
     SL.Values['SellerId']:= FSellerId; 
     FOR i:=0 TO FMarketplaceIds.Count-1 DO 
     begin 
       SL.Values['MarketplaceId.Id.' + IntToStr(i+1)]:= FMarketplaceIds[i]; 
     end; 

     SL.Values['Timestamp']:= GenerateTimeStamp(Now); 
     SL.Values['SignatureMethod']:= 'HmacSHA256'; 
     SL.Values['SignatureVersion']:= '2'; 
     SL.Values['Version']:= AVersion; 

     FOR i:=0 TO SL.Count-1 DO 
     begin 
       AKey:= UrlEncode(SL.Names[i]); 
       AValue:= UrlEncode(SL.ValueFromIndex[i]); 
       SL[i]:= AKey + '=' + AValue; 
     end; 

     SortList(SL); 
     SL.Delimiter:= '&'; 
     AQuery:= SL.DelimitedText; 

     AStrToSign:= AMethod + #10 + AHost + #10 + AURI + #10 + AQuery; 
     TgboUtil.ShowMessage(AStrToSign); 

     ASignature:= GenerateSignature(AStrToSign, FAWSSecretKey); 
     TgboUtil.ShowMessage(ASignature); 

     APath:= 'https://' + AHost + AURI + '?' + AQuery + '&Signature=' + Urlencode(ASignature); 
     TgboUtil.ShowMessage(APath); 
    finally 
     SL.Free; 
    end; 

    AHTTP:= TIdHTTP.Create(nil); 
    try 
     AHTTP.IOHandler := TIdSSLIOHandlerSocketOpenSSL.Create(AHTTP); 
     AHTTP.Request.ContentType:= 'text/xml'; 
     AHTTP.Request.Connection:= 'Close'; 
     AHTTP.Request.CustomHeaders.Add('x-amazon-user-agent: MyApp/1.0 (Language=Delphi/XE7)'); 
     AHTTP.HTTPOptions:= AHTTP.HTTPOptions + [hoKeepOrigProtocol]; 
     AHTTP.ProtocolVersion:= pv1_0; 
     AStream:= TStringStream.Create; 
     AResultStream:= TStringStream.Create; 
     try 
      AHTTP.Post(APath, AStream, AResultStream); 
      Result:= AResultStream.DataString; 
      ShowMessage(Result); 
     finally 
      AStream.Free; 
      AResultStream.Free; 
     end; 
    finally 
     AHTTP.Free; 
    end; 
end; 

urlencode und GenerateTimestamp sind meine eigenen Funktionen und sie tun, was der Name verspricht, SortList mein eigenes Verfahren, das sortiert Die Stringliste in einer Byte-Reihenfolge, wie von Amazon angefordert, TgboUtil.ShowMessage ist meine eigene ShowMessage-Alternative, die die vollständige Nachricht mit allen Zeichen zeigt und nur zum Debuggen verwendet wird. Das HTTP-Protokoll ist 1.0 nur zum Testen, weil ich eine 403 (Berechtigung verweigert) als HTTP-Rückgabe früher erhalten habe. Ich wollte das nur als Problem ausschließen, wie die Indy-Dokumentation besagt, dass die Protokollversion 1.1 wegen problematischer Serverantworten als unvollständig angesehen wird.

Es gibt mehrere Beiträge in Bezug auf die amazon mws Thema hier, aber dieses spezifische Problem scheint neu zu sein.

Diese Frage hier kann jemandem helfen, der gerade nicht so weit gekommen ist, aber ich hoffe auch, dass jemand eine Lösung anbieten kann, um den gleichen Signaturwert in Delphi zu erhalten, wie ich es mit PHP bekommen habe.

Vielen Dank im Voraus.

+0

Bitte beachten Sie die Antwort von Remy Lebeau für einige gute Änderungen meines Codes erklärt, so wird es funktionieren in das Ende. Damit interessierte Besucher das Problem und seine Lösung besser verstehen können, habe ich beschlossen, den Code unbearbeitet zu lassen, mit Ausnahme des Lecks von 'TIdSSLIOHandlerSocketOpenSSL' – KaiW

Antwort

3

Mit dem letzten SVN-Snapshot von Indy 10 kann ich Ihr Signaturproblem nicht reproduzieren. Wenn Sie UTF-8 verwenden, erzeugen Ihre Beispielschlüssel + Wertdaten dasselbe Ergebnis in Delphi wie die PHP-Ausgabe. Also, Ihre GenerateSignature() Funktion ist in Ordnung, vorausgesetzt, dass:

  1. Sie verwenden IndyTextEncoding_UTF8 statt IndyTextEncoding_UTF16LE.

  2. Sie sicherstellen, dass AData und AKey gültige Eingabedaten enthalten.

Außerdem sollten Sie sicherstellen, dass TIdHashSHA256.IsAvailable gibt true zurück, sonst TIdHashHMACSHA256.HashValue() fehl. Dies kann beispielsweise passieren, wenn OpenSSL nicht geladen werden kann.

Versuchen Sie stattdessen:

function GenerateSignature(const AData, AKey: string): string; 
var 
    AHMAC: TIdBytes; 
begin 
    IdSSLOpenSSL.LoadOpenSSLLibrary; 

    if not TIdHashSHA256.IsAvailable then 
    raise Exception.Create('SHA-256 hashing is not available!'); 

    with TIdHMACSHA256.Create do 
    try 
    Key := IndyTextEncoding_UTF8.GetBytes(AKey); 
    AHMAC := HashValue(IndyTextEncoding_UTF8.GetBytes(AData)); 
    finally 
    Free; 
    end; 

    Result := TIdEncoderMIME.EncodeBytes(AHMAC); 
end; 

Davon abgesehen, gibt es durchaus ein paar Probleme mit Ihrem MwsRequest() Funktion:

  1. Sie sind undicht die TIdSSLIOHandlerSocketOpenSSL Objekt. Sie weisen ihm kein zu, und TIdHTTP übernimmt kein Eigentum, wenn es seiner Eigenschaft IOHandler zugewiesen ist. Tatsächlich ist das Zuweisen der IOHanlder in Ihrem Beispiel tatsächlich optional, siehe New HTTPS functionality for TIdHTTP für warum.

  2. Sie setzen AHTTP.Request.ContentType auf den falschen Medientyp. Sie senden keine XML-Daten. Legen Sie daher den Medientyp nicht auf 'text/xml' fest. In diesem Fall müssen Sie stattdessen den Wert festlegen.

  3. Wenn Sie AHTTP.Post() aufrufen, ist Ihr AStream-Stream leer, sodass Sie keine Daten auf dem Server veröffentlichen. Sie setzen Ihre AQuery Daten in die Abfragezeichenfolge der URL selbst, aber es gehört tatsächlich in AStream stattdessen. Wenn Sie die Daten in der URL-Abfragezeichenfolge senden möchten, müssen Sie TIdHTTP.Get() anstelle von TIdHTTP.Post() verwenden und Ihren Wert AMethod in 'GET' anstelle von 'POST' ändern.

  4. Sie verwenden die Version TIdHTTP.Post(), die einen Ausgang TStream füllt. Sie verwenden eine TStringStream, um die Antwort auf eine String ohne Rücksicht auf den tatsächlichen Zeichensatz, der von den Antwortdaten verwendet wird, zu konvertieren. Da Sie kein TEncoding Objekt im TStringStream Konstruktor angeben, wird es TEncoding.Default für die Decodierung verwenden, die möglicherweise nicht (und wahrscheinlich nicht) mit dem tatsächlichen Zeichensatz der Antwort übereinstimmt. Sie sollten stattdessen die andere Version von Post() verwenden, die String zurückgibt, sodass die Antwortdaten basierend auf dem tatsächlichen Zeichensatz, der vom HTTPS-Server gemeldet wird, dekodieren kann.

stattdessen etwas mehr wie diese versuchen:

function TgboAmazon.MwsRequest(const AFolder, AVersion: string; 
    const AParams: TStringList; const AEndPoint: string): string; 
var 
    i: Integer; 
    SL: TStringList; 
    AMethod, AHost, AURI, AQuery, AStrToSign, APath, ASignature: string; 
    AHTTP: TIdHTTP; 
begin 
    AMethod := 'POST'; 
    AHost := AEndPoint; 
    AURI := '/' + AFolder + '/' + AVersion; 

    AQuery := ''; 
    SL := TStringList.Create; 
    try 
    SL.Assign(AParams); 

    SL.Values['AWSAccessKeyId'] := FAWSAccessKeyId; 
    SL.Values['SellerId'] := FSellerId; 
    for i := 0 to FMarketplaceIds.Count-1 do 
    begin 
     SL.Values['MarketplaceId.Id.' + IntToStr(i+1)] := FMarketplaceIds[i]; 
    end; 

    SL.Values['Timestamp'] := GenerateTimeStamp(Now); 
    SL.Values['SignatureMethod'] := 'HmacSHA256'; 
    SL.Values['SignatureVersion'] := '2'; 
    SL.Values['Version'] := AVersion; 
    SL.Values['Signature'] := ''; 

    SortList(SL); 

    for i := 0 to SL.Count-1 do 
     SL[i] := UrlEncode(SL.Names[i]) + '=' + UrlEncode(SL.ValueFromIndex[i]); 

    SL.Delimiter := '&'; 
    SL.QuoteChar := #0; 
    SL.StrictDelimiter := True; 
    AQuery := SL.DelimitedText; 
    finally 
    SL.Free; 
    end; 

    AStrToSign := AMethod + #10 + Lowercase(AHost) + #10 + AURI + #10 + AQuery; 
    TgboUtil.ShowMessage(AStrToSign); 

    ASignature := GenerateSignature(AStrToSign, FAWSSecretKey); 
    TgboUtil.ShowMessage(ASignature); 

    APath := 'https://' + AHost + AURI; 
    TgboUtil.ShowMessage(APath); 

    AHTTP := TIdHTTP.Create(nil); 
    try 
    // this is actually optional in this example... 
    AHTTP.IOHandler := TIdSSLIOHandlerSocketOpenSSL.Create(AHTTP); 

    AHTTP.Request.ContentType := 'application/x-www-form-urlencoded'; 
    AHTTP.Request.Connection := 'close'; 
    AHTTP.Request.UserAgent := 'MyApp/1.0 (Language=Delphi/XE7)'; 
    AHTTP.Request.CustomHeaders.Values['x-amazon-user-agent'] := 'MyApp/1.0 (Language=Delphi/XE7)'; 
    AHTTP.HTTPOptions := AHTTP.HTTPOptions + [hoKeepOrigProtocol]; 
    AHTTP.ProtocolVersion := pv1_0; 

    AStream := TStringStream.Create(AQuery + '&Signature=' + Urlencode(ASignature); 
    try 
     Result := AHTTP.Post(APath, AStream); 
     ShowMessage(Result); 
    finally 
     AStream.Free; 
    end; 
    finally 
    AHTTP.Free; 
    end; 
end; 

Da jedoch die Antwort als XML dokumentiert ist, wäre es besser, die Antwort an den Anrufer als TStream zurückzukehren (nicht TStringStream mit (obwohl) oder TBytes anstelle von String. Auf diese Weise kann Ihr XML-Parser die Bytes raw bytes dekodieren. XML hat seine eigene charset Regeln, die von HTTP getrennt sind, so lassen Sie den XML-Parser für Sie seine Arbeit tun:

procedure TgboAmazon.MwsRequest(...; Response: TStream); 
var 
    ... 
begin 
    ... 
    AHTTP.Post(APath, AStream, Response); 
    ... 
end; 
+0

Vielen Dank für Ihre gute und ausführliche Antwort. Ich habe so viele Variationen des Codes mit verschiedenen Zeichensätzen probiert, um das Problem zu lösen, aber es scheint, dass ich am Ende die nächste Lösung nicht verwendet habe. Jetzt mit UTF8 funktioniert die 'GenerateSignature' gut für mich. Ich akzeptiere auch Ihre Änderungen und ändere meinen Code wie Sie vorgeschlagen haben. Ich habe 'text/xml' als Inhaltstyp verwendet, weil Amazon dies explizit angefordert hat. Aber es funktioniert auch mit 'application/x-www-form-urlencoded', so scheint es, dass es ihnen am Ende egal ist und ich benutze es wie vorgeschlagen. – KaiW

Verwandte Themen