How to set body data indy HTTPS post - delphi

So I've looked around, and the only question describing my problem is 6 years old with 0 answers, so I guess I will try again.
I am using delphi 2009 with Indy10.
I am trying to post JSON to an api using HTTPS.
Instance.FHTTP := TIdHTTP.Create;
Instance.FHTTP.IOHandler := TIdSSLIOHandlerSocketOpenSSL.Create(Instance.FHTTP);
{$IFDEF DEBUG}
Instance.FHTTP.ProxyParams.ProxyPort := 8888;
Instance.FHTTP.ProxyParams.ProxyServer := '127.0.0.1';
{$ENDIF}
Instance.FHTTP.Request.ContentType := 'application/json';
Instance.FAccessToken := Instance.FHTTP.Post('https://somedomain.com/api/endpoint', '{JSONName: JSONValue }' );
I have seen many answers suggesting that the JSON payload should be given as a string param in the TidHTTP.Postmethod, but when i try that, it expects a filepath, and throws an error saying:
'Cannot open file "[path to project{JSONName:JSONValue }]". The specified file was not found'.
If i add my JSON to a TStringList and add give that as a parameter, it simply adds the JSON to the header of the request.
Any help is greatly appreciated.

The Post overload that takes a second string indeed interprets it as a filename:
function Post(AURL: string; const ASourceFile: String): string; overload;
That's why this doesn't work. You need to instead use the overload that takes a TStream:
function Post(AURL: string; ASource: TStream): string; overload;
You can put your JSON in a TStringStream:
StringStream := TStringStream.Create('{JSONName: JSONValue }', TEncoding.UTF8);
try
Instance.FAccessToken := Instance.FHTTP.Post('https://somedomain.com/api/endpoint', StringStream);
finally
StringStream.Free;
end;

Related

Creating an amazon MWS signature with Delphi XE7 and Indy classes

I need to generate a signature for amazon MWS and decided to find a solution with only the components and classes which come with Delphi. Because I am using Indy for the HTTP post itself, it seemed to be a good idea to use Indy classes for the calculation of the RFC 2104-compliant HMAC.
For others, who work on amazon integration, the creation of the "Canonicalized Query String" is explained in the amazon tutorial very well: http://docs.developer.amazonservices.com/en_DE/dev_guide/DG_ClientLibraries.html
Be careful, just use #10 for line breaking, as #13#10 or #13 will fail with a wrong signature. It may also be important to add ":443" to the amazon Endpoint (Host), depending on the TIdHttp version, as explained in question #23573799.
To create a valid signature, we have to calculate a HMAC with SHA256 with the query string and the SecretKey we got from amazon after registration and then, the result has to be encoded in BASE64.
The query string is properly generated and identical to the string the amazon Scratchpad creates. But the call failed because the signature is not correct.
After some tests I realized that the signature I got from my query string is not the same as the result I got when I used PHP to generate it. The PHP result is considered as correct, as my PHP solution simply works with amazon since a long time, the Delphi result is different, which is not correct.
To make testing easier, I use '1234567890' as value for the query string and 'ABCDEFG' as replacement for the SecretKey. When the result I get with Delphi is the same as the result I get with PHP, the problem should be solved, I believe.
Here is how I get the correct result with PHP:
echo base64_encode(hash_hmac('sha256', '1234567890', 'ABCDEFG', TRUE));
This shows a result of
aRGlc3RY1pKmKX0hvorkVKNcPigiJX2rksqXzlAeCLg=
The following Delphi XE7 code returns the wrong result, while using the indy version that comes with 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;
Here the result, which is shown in a Memo with
Memo.Lines.Text:= GenerateSignature('1234567890', 'ABCDEFG');
is:
jg6Oddxvv57fFdcCPXrqGWB9YD5rSvtmGnZWL0X+y0Y=
I believe the problem has something to do with the encodings, so I have done some research around that. As the amazon tutorial (link see above) tells, amazon expects a utf8 encoding.
As the Indy function "ToBytes" expect a string, which is a UnicodeString in my Delphi version, I quit testing with other string types as UTF8String for parameters or variables, but I just do not know where utf8 should come into place. Also I do not know if the encodings I use in the code above are the correct ones.
I choose UTF16LE because UnicodeString is utf16 encoded (see http://docwiki.embarcadero.com/RADStudio/Seattle/en/String_Types_(Delphi) for details) and LE (Little-Endian) is most commonly used on modern machines. Also the TEncodings of Delphi itself there is "Unicode" and "BigEndianUnicode", so "Unicode" seems to be LE and some kind of "standard" Unicode.
Of course I tested to use IndyTextEncoding_UTF8 instead of IndyTextEncoding_UTF16LE in the code above, but it does not work anyway.
Because
TIdEncoderMIME.EncodeBytes(AHMAC);
is writing the TidBytes to a Stream first and then reading it all with 8bit encoding, this could be a source of problem also, so I also tested with
Result:= BytesToString(AHMAC, IndyTextEncoding_UTF16LE);
Result:= TIdEncoderMIME.EncodeString(Result, IndyTextEncoding_UTF16LE);
but the result is the same.
If you like to see the main code for creating the request, here it is:
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 and GenerateTimestamp are my own functions and they do what the name promises, SortList is my own procedure which sorts the stringlist in a byte order as requested by amazon, TgboUtil.ShowMessage is my own ShowMessage alternative which shows the complete message with all characters and is used for debugging only. The http protocol is 1.0 for testing only, because I got a 403 (permission denied) as HTTP return earlier. I just wanted to exclude this as problem as the indy documentation said, that protocol version 1.1 is considered incomplete because of problematic server answers.
There are several posts regarding the amazon mws topic here, but that specific problem seems to be new.
This question here may help someone who just not have come so far, but also I hope that someone can provide a solution to just get the same signature value in Delphi as I got with PHP.
Thank you in advance.
Using the latest SVN snapshot of Indy 10, I am not able to reproduce your signature problem. When using UTF-8, your example key+value data produces the same result in Delphi as the PHP output. So, your GenerateSignature() function is fine, provided that:
you use IndyTextEncoding_UTF8 instead of IndyTextEncoding_UTF16LE.
you make sure that AData and AKey contain valid input data.
Also, you should make sure that TIdHashSHA256.IsAvailable returns true, otherwise TIdHashHMACSHA256.HashValue() will fail.
this could happen, for instance, if OpenSSL fails to load.
Try this instead:
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;
That being said, there are quite a few problems with your MwsRequest() function:
you are leaking the TIdSSLIOHandlerSocketOpenSSL object. You are not assigning an Owner to it, and TIdHTTP does not take ownership when assigned to its IOHandler property. In fact, assigning the IOHanlder is actually optional in your example, see New HTTPS functionality for TIdHTTP for why.
you are setting AHTTP.Request.ContentType to the wrong media type. You are not sending XML data, so don't set the media type to 'text/xml'. In this situation, you need to set it to 'application/x-www-form-urlencoded' instead.
when calling AHTTP.Post(), your AStream stream is empty, so you are not actually posting any data to the server. You are putting your AQuery data in the query string of the URL itself, but it actually belongs in AStream instead. If you want to sent the data in the URL query string, you have to use TIdHTTP.Get() instead of TIdHTTP.Post(), and change your AMethod value to 'GET' instead of 'POST'.
you are using the version of TIdHTTP.Post() that fills an output TStream. You are using a TStringStream to convert the response to a String without any regard to the actual charset used by the response data. Since you are not specifying any TEncoding object in the TStringStream constructor, it will use TEncoding.Default for decoding, which may not (and likely will not) match the response's actual charset. You should instead use the other version of Post() that returns a String so TIdHTTP can decode the response data based on the actual charset reported by the HTTPS server.
Try something more like this instead:
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;
However, since the response is documented as being XML, it would be better to return the response to the caller as a TStream (not using TStringStream, though) or TBytes instead of as a String. That way, instead of Indy decoding the bytes, let your XML parser decode the raw bytes on its own. XML has its own charset rules that are separate from HTTP, so let the XML parser do its job for you:
procedure TgboAmazon.MwsRequest(...; Response: TStream);
var
...
begin
...
AHTTP.Post(APath, AStream, Response);
...
end;

Create Google-Maps-API signature with Delphi

I would like to create signatures for my maps-for-work clientid. I need to get the geolocation-api running with my clientid. The API is called for example like this:
https://maps.googleapis.com/maps/api/geocode/json?address=1600+Amphitheatre+Parkway,+Mountain+View,+CA&client=gme-myPersonalclientid&signature=HereIsMyProblem
I call this link from my Delphi application.
I tried to transfer phyton/java/c# from the official helpsite (https://developers.google.com/maps/documentation/static-maps/get-api-key?hl=en) to Delphi but I am still missing something.
function TMgrGoogleMap.SignUrl(AUrl: String): String;
Var
CryptoKey: String;
Signature: String;
HMac: TIdHMACSHA1;
CryptoIDByte: TBytes;
SignatureHash: TidBytes;
begin
// IdSSLOpenSSL.LoadOpenSSLLibrary; // not sure if needed, think not
CryptoKey := 'ARandomKryptoKeyfY1XbqTib1QlY=';
CryptoIDByte := TNetEncoding.Base64.DecodeStringToBytes(CryptoKey);
HMac := TIdHMACSHA1.Create;
HMac.Key := TIdBytes(CryptoIDByte);
SignatureHash := HMac.HashValue(TidBytes(AUrl));
Signature := TNetEncoding.Base64.EncodeBytesToString(SignatureHash);
Result := AUrl + '&signature=' + Signature;
end;
As Google-Help code-examples tell, I first decode my cryptokey with Base64, then I create a hash-value with SHA-1 with the encoded cryptokey and the url.
Afterwards I encode it again with Base64 but the result is unfortunately another then the onlinetest https://m4b-url-signer.appspot.com/ gives.
What am I missing? Thanks for every answer in advance.

IdHTTP.Put Error: HTTP/1.1405 Method Not Allowed

In Delphi XE2, I am trying to upload the lines of a memo to a file on my webspace with IdHTTP.Put:
procedure TForm1.btnUploadClick(Sender: TObject);
var
StringToUpload: TStringStream;
begin
StringToUpload := TStringStream.Create('');
try
StringToUpload.WriteString(memo.Lines.Text);
// Error: HTTP/1.1 405 Method Not Allowed.
IdHTTP1.Put(edtOnlineFile.Text, StringToUpload);
finally
StringToUpload.Free;
end;
end;
But I always get this error message:
So what must I do to avoid the error and make the upload?
It means the HTTP server does not support the PUT method on that URL (if at all). There is nothing you can do about that. You will likely have to upload your data another way, usually involving POST instead, or a completely different protocol, like FTP.
BTW, when using TStringStream like this, don't forget to reset the Position if you use the WriteString() method:
StringToUpload.WriteString(memo.Lines.Text);
StringToUpload.Position := 0;
Otherwise, use the constructor instead:
StringToUpload := TStringStream.Create(memo.Lines.Text);
Thanks for the above code, here is perhaps a little more information with a little helper function to assist with that Stream constructor which I found works for any string you pass through, even it contains binary stuff.
//Helper function to make JSON string correct for processing with POST / GET
function StringToStream(const AString: string): TStream;
begin
Result := TStringStream.Create(AString);
end;
//somewhere in your code, I am posting to Spring REST, encoding must be utf-8
IdHTTP1.Request.ContentType := 'application/json'; //very important
IdHTTP1.Request.ContentEncoding := 'utf-8'; //which encoding?
response := IdHTTP1.Put(URL, StringToStream(body)); //response,URL,body are declared as String

Binary to Base64 (Delphi)

How can I get content of an exe file and convert it into Base64 encoding ?
Edit
I use D2010 and I want to know how is it possible exactly ?
open an exe file
convert its content into base64
In Delphi 2009/2010/XE there is unit EncdDecd.pas (Soap.EncdDecd.pas for Delphi XE2) containing the functions EncodeBase64 and DecodeBase64. You can load the exe file into a memorystream and then call EncodeBase64.
function EncodeFile(const FileName: string): AnsiString;
var
stream: TMemoryStream;
begin
stream := TMemoryStream.Create;
try
stream.LoadFromFile(Filename);
result := EncodeBase64(stream.Memory, stream.Size);
finally
stream.Free;
end;
end;
In ancient Delphi versions, you can use synapse (link here)
Just put synacode.pas in your uses e call EncodeBase64/EncodeBase64.
Cheers
As also mentioned in the comments, since Delphi XE8 you can use the System.NetEncoding.TNetEncoding.Base64 class property.
It also returns a string instead of an AnsiString:
function TryEncodeFile(const AFileName: string; out ABase64string: string): Boolean;
var
MemStream: TMemoryStream;
begin
MemStream := TMemoryStream.Create;
try
MemStream.LoadFromFile(AFileName);
ABase64string :=
TNetEncoding.Base64.EncodeBytesToString(MemStream.Memory, MemStream.Size);
Result := True;
finally
MemStream.Free;
end;
end;

File MD5 checksum

In this question is mentioned the wcrypt2.
What I need is simply calculate the MD5 of a file. It would be perfect if I could calculate it without having to save it because it is a downloaded file in stream format.
I would like to have the most straightforward way to do that.
Thanks!
Here is a working code for Indy 10:
function MD5File(const FileName: string): string;
var
IdMD5: TIdHashMessageDigest5;
FS: TFileStream;
begin
IdMD5 := TIdHashMessageDigest5.Create;
FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Result := IdMD5.HashStreamAsHex(FS)
finally
FS.Free;
IdMD5.Free;
end;
end;
Regards,
OscaR1
Based on #dummzeuch answere I wrote this function:
function getMD5checksum(s: TStream): string;
var
md5: TIdHashMessageDigest5;
hash : T4x4LongWordRecord;
begin
md5 := TIdHashMessageDigest5.Create;
s.Seek(0,0);
hash := md5.HashValue(s);
result := IntToHex(Integer(hash[0]), 4) +
IntToHex(Integer(hash[1]), 4) +
IntToHex(Integer(hash[2]), 4) +
IntToHex(Integer(hash[3]), 4);
end;
Indy comes with functions for calculating several hashes, MD5 is one of them. Indy is included in all versions of Delphi since at least Delphi 2006 and available as a free download for older versions.
What about:
function GetFileMD5(const Stream: TStream): String; overload;
var MD5: TIdHashMessageDigest5;
begin
MD5 := TIdHashMessageDigest5.Create;
try
Result := MD5.HashStreamAsHex(Stream);
finally
MD5.Free;
end;
end;
function GetFileMD5(const Filename: String): String; overload;
var FileStream: TFileStream;
begin
FileStream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Result := GetFileMD5(FileStream);
finally
FileStream.Free;
end;
end;
As you mentioned, the post you linked to talks about wcrypt2, which is a library of cryptographic routines, including MD5. The post you linked to also seems to indicate that it is available for Delphi 7 since the asker includes output labeled "Delphi 7." You have tagged this question delphi7, so I assume that's the version you're using, too. So what's stopping you from using wcrypt2?
The question links to a copy of wcrypt2.pas, and the copyright dates in that file appear to indicate that the unit was available by the time Delphi 7 was released. Check your installation; you might already have it. If not, then the unit also says that it was obtained via Project Jedi, so you could try looking there for the unit as well.
The answers to your referenced question include example Delphi code and the names of units that come with Delphi for doing MD5. They come with Delphi 2009, so you should check whether they're also available for your version.
Take a look at this implementation of MD5SUM in Delphi. It requires a string for input, but I imagine you can easily make it work with a stream.
MessageDigest_5 would work for this as well.
I use the following function in Delphi 7 with Indy 10.1.5
uses IdHashMessageDigest, idHash, Classes;
...
function cc_MD5File(const p_fileName : string) : string;
//returns MD5 has for a file
var
v_idmd5 : TIdHashMessageDigest5;
v_fs : TFileStream;
v_hash : T4x4LongWordRecord;
begin
v_idmd5 := TIdHashMessageDigest5.Create;
v_fs := TFileStream.Create(p_fileName, fmOpenRead OR fmShareDenyWrite) ;
try
v_hash := v_idmd5.HashValue(v_fs);
result := v_idmd5.AsHex(v_hash);
finally
v_fs.Free;
v_idmd5.Free;
end;
end;
If you use Overbyte http://www.overbyte.eu/frame_index.html just add unit and call function FileMD5 with name of file
uses OverbyteIcsMd5;
....
function GetMd5File:String;
begin
Result := FileMD5(FileName);
end;

Resources