Delphi XE2: How to define custom DataSnap REST URI? - delphi

I am using Delphi XE2 to write DataSnap REST service. I notice that the REST URI in DataSnap must strictly follow this format (refer here):
http://my.site.com/datasnap/rest/URIClassName/URIMethodName[/inputParameter]*
A famous example is sample method create by DataSnap server wizard:
http://my.site.com/datasnap/rest/TServerMethods1/ReverseString/ABC
There are 2 common ways to supply parameters in URI:
Path Segment parameter: /TServerMethods1/ReverseString/ABC
Query String parameter: /TServerMethods1/customers?name=bill
The Path Segment parameter URI is definitely supported by DataSnap REST. Is Query string parameters URI support in DataSnap REST too?
I have the following REST URI example and found it seems impossible to make it work with current DataSnap REST library:
/customers/A1234
return customer object of ID A1234
/customers/A1234.xml
return customer object of ID A1234 in XML format
/customers/A1234.json
return customer object of ID A1234 in json format
/customers/A1234.html
return customer object of ID A1234 in html format
/customers?name=Bill
return a list of customer whose name contain Bill

I don't know how to do it using DataSnap, but there are ways around it. You can put something called URLRewrite to good use for this as both your friendly URI's and the ones required by DataSnap are easily mappable.
For IIS you can use (enable) the URLRewrite module which is standard in IIS 7. More information can be found on the official site: http://www.iis.net/download/urlrewrite.
Be sure to create rules for inbound and outbound URI's so that the "internal" (Datasnap) URI's do not get out into the wild.
If you are running the site on Apache, similar functionality is available, and I thin you need to amend the .htaccess file, but I have no experience with Apache so I could be wrong.

A bit late to the party, but yes you can use query parameters.
You have to use GetInvocationMetadata.QueryParams
see the example below.
uses DBXPlatform;
function TServerMethods1.EchoString(Value: string): string;
var
metaData: TDSInvocationMetadata;
i: integer;
begin
metaData := GetInvocationMetadata;
for i := 0 to Pred(metaData.QueryParams.Count) do
begin
Result := Result + '<param>' + metaData.QueryParams[i] + '</param>';
end;
metaData.ResponseContent := '<xml>' + Result + '</xml>';
end;

Related

TIdHTTP Post to API and save response as a .pdf file

I'm calling an API URL with some parameters according to the documentation provided.
The way the API is set up, the response should be an auto-download of a .pdf file.
The parameters are:
number - which stands for a 13 digit number that represents the unique file indicator in their system (example: 5277110610029)
username and user_pass respectively - which stand for the login credentials to access the system
client_id - which stands for a unique client ID associated with my account in their system
language - where I indicate the language the file contents should be in (English or other of the available languages)
Documentation indicates it should be made as a Post request.
I have the following code:
var
FileName : string;
URL: String;
Params: TStringList;
memStream: TMemoryStream;
...
begin
FileName:= MainModule.PDFfileName + '.pdf';
begin
URL := 'https://someURL/view_integrated_pdf.php?';
Params := TStringList.Create;
memStream := TMemoryStream.Create;
try
Params.Add('number='+MainModule.number+'');
Params.Add('username='+MainModule.User+'');
Params.Add('client_id='+MainModule.clientID+'');
Params.Add('user_pass='+MainModule.Pass+'');
Params.Add('language=en');
MainModule.IdHTTP.Post(URL, Params, memStream);
finally
memStream.SaveToFile(ServerModule.FilesFolderPath+'\pdfs\'+FileName);
Params.Free;
memStream.Free;
end;
end;
pdfForm.ShowModal();
end;
If I try the resulting URL and parameters in the browser - it auto-downloads the pdf file the API gives me with the name numberParameter.pdf
If I do it in Delphi using the provided code, 8 out of 10 times it saves a pdf file with a 1 KB Size (normally it is between 32 and 100 for the successful file) and when I try to open it in the program using my pdfForm and subsequent viewer, the viewer throws an error "Invalid PDF structure"
What am I doing wrong? / How do you properly save a Post requests that returns a .pdf file to download from the API?
UPDATE
As per the comments, opening up the 1kb resulting PDF in Notepad++ displays the contents as simply Error username.
This is puzzling since I checked and the username being passed is accurate + if I paste the exact same URL with parameter values filed, in the browser, it works perfectly and gives me the correct PDF.
Is my code not the correct approach to Post and save the file being sent?
If I try the resulting URL and parameters in the browser - it auto-downloads the pdf file the API gives me with the name numberParameter.pdf
The only way to do that in a browser is to put the parameters in the URL's query component, eg:
https://someURL/view_integrated_pdf.php?number=...&username=...&client_id=...&user_pass=...&language=en
... and not in a POST body, as your code is doing. Also, a browser would send a GET request, not a POST request. Unless you are filling in an HTML webform (ie <form action="<URL>" method="POST" ...>) and submitting it. Is that what the API documentation says to do?
Since you did not provide any details from the documentation about what this server is actually expecting, we can't really tell you whether your code is correct or wrong. But it does seem that your manual tests are contradicting what you claim the documentation says the server is expecting.
If I do it in Delphi using the provided code, 8 out of 10 times it saves a pdf file with a 1 KB Size (normally it is between 32 and 100 for the successful file) and when I try to open it in the program using my pdfForm and subsequent viewer, the viewer throws an error "Invalid PDF structure"
From comments, you say your file is receiving a textual error message. TIdHTTP would save such text to your TMemoryStream ONLY IF either:
the HTTP server is NOT reporting an error at the HTTP level, ie it is sending the text message using an HTTP 2xx success response. I suspect this is what is happening in your case. By default, if the server uses a proper HTTP error code, TIdHTTP will raise an EIdHTTPProtocolException containing the text message and NOT save the text to your TMemoryStream at all.
the HTTP server IS reporting an error at the HTTP level, but you are using the hoNoProtocolErrorException and hoWantProtocolErrorContent flags together in the TIdHTTP.HTTPOptions property. In which case, TIdHTTP would not raise EIdHTTPProtocolException and would instead save whatever data the server sends as-is to your TMemoryStream.
Since there is clearly no HTTP exception being raised, you will have to validate the server's response before you can use the downloaded data in your pdfForm, ie by looking at the TIdHTTP.Response.ContentType and/or TIdHTTP.Response.ContentDisposition property to know whether the server actually sent a PDF file or not.
This is puzzling since I checked and the username being passed is accurate + if I paste the exact same URL with parameter values filed, in the browser, it works perfectly and gives me the correct PDF.
Well, for one thing, you have a typo in your code: the numberr field needs to be number instead.
Beyond that, putting the URL in a browser is NOT the same operation that your code is doing, so try changing your code to mimic what you are doing manually, eg:
uses
..., IdGlobalProtocols, IdHTTP, IdURI;
...
var
URL : string;
memStream: TMemoryStream;
begin
// All parameters into the URI for a HTTP GET request
URL := 'https://someURL/view_integrated_pdf.php'
+ '?number=' + TIdURI.ParamsEncode(MainModule.number)
+ '&username=' + TIdURI.ParamsEncode(MainModule.User)
+ '&client_id=' + TIdURI.ParamsEncode(MainModule.clientID)
+ '&user_pass=' + TIdURI.ParamsEncode(MainModule.Pass)
+ '&language=en';
memStream := TMemoryStream.Create;
try
MainModule.IdHTTP.Get(URL, memStream);
// Is it really PDF? Other formats such as plaintext is not wanted.
if not IsHeaderMediaType(MainModule.IdHTTP.ContentType, 'application/pdf') then Exit;
memStream.SaveToFile(ServerModule.FilesFolderPath + '\pdfs\' + MainModule.PDFfileName + '.pdf');
finally
memStream.Free;
end;
pdfForm.ShowModal;
end;
If that does not work, then please update your question to provide the actual documentation.

Delphi Indy https request with custom DNS resolving

example url:
https://api.binance.com/api/v1/time
Using TIdDNSResolver and cloudflare dns I can get the host IP.
direct request in the form of
https://205.251.219.20/api/v1/time
doesn't work cause as I understand the server expects to see "api.binance.com" in the url. (it doesnt work even in browser)
Using synapce and the following patch to replace request's host with resolved IP I make it working
function THTTPSend.InternalDoConnect(needssl: Boolean): Boolean;
begin
Result := False;
FSock.CloseSocket;
FSock.Bind(FIPInterface, cAnyPort);
if FSock.LastError <> 0 then
Exit;
If ResolvedIP.IsEmpty
then FSock.Connect(FTargetHost, FTargetPort)
else FSock.Connect(ResolvedIP, FTargetPort); // !!
Is there any way to do the same using Indy?
By default, TIdHTTP uses the Host:Port that is specified/inferred by the URL that is being requested. To change that behavior, you would have to alter TIdHTTP's source code, such as in the TIdCustomHTTP.SetHostAndPort() or TIdCustomHTTP.CheckAndConnect() method (which are both not virtual), and then recompile Indy.
Alternatively, you could use TIdHTTP's OnSocketAllocated or On(Before|After)Bind event (which are not promoted to published in TIdHTTP, so you would have to access them manually at runtime) to change the TIdHTTP.IOHandler.Host property to whatever IP address you want to connect to. This will not have any affect on the HTTP headers that TIdHTTP sends, such as Host, which will still be taken from the URL.
Alternatively, if you want all of Indy's Host-to-IP DNS queries to go through Cloudflare, you could derive a custom class from TIdStack (or whatever platform-specific descendant you are interested in, such as TIdStackWindows), override its virtual HostByName() method to perform whatever DNS query you want, and then pass your class type to the SetStackClass() function in the IdStack unit at program startup before any Indy classes are instantiated.

Amazon MWS API Call Using Delphi/Indy

I'm developing a simple application to "talk" to the Amazon MWS API. Because a lot of existing code is at play here, I need to get this done in Delphi 2010 with Indy 10 (10.5.5) components, which I have used successfully to integrate with many other APIs in the past. However, the Amazon API seems to be incredibly sensitive to the smallest of details, to the point that all my calls are being denied with the already infamous "SignatureDoesNotMatch" error message.
Here's what I have accomplished so far:
1) My app will assemble a request, sign it with HMAC-SHA256 (using the OpenSSL libraries) and send it to the Amazon server endpoint.
2) The HMAC signature alone proved to be a challenge in itself, but it's now working correctly 100% of the time (as verified against requests generated by the Amazon Scrachpad).
However, as I pointed out earlier, my requests are always rejected by the MWS server with the SignatureDoesNotMatch error, even though they are verifiably correct. The only thing I can think of that could be causing problems is the way Indy may be handling the POST requests, specifically the text encoding process.
Has anyone been successful in connecting a Delphi/Indy client to MWS? If so, what kind of TIdHTTP settings were used? Here's what I have:
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;
If I assemble a GetOrder call in Scratchpad, then paste the timestamp of that call into the code above, I get EXACTLY the same query string here, with the same signature and size, etc. But my Indy request must be encoding things differently, because the MWS server doesn't like the call.
I know MWS is at least "reading" the query string, because if I change the timestamp to an old date, it returns a "request expired" error instead.
Amazon's tech support is clueless, posting a message every day with basic stuff like "Make sure the secret key is correct" (as if getting a signature with HMAC-SHA256 and MD5 would work without a valid key!!!!).
One more thing: if I use Wireshark to "watch" the raw request from both the code above and the C-Sharp Amazon sample code, I can't tell a difference either. However, I'm not sure Wireshark makes a distinction between UTF-8 and ASCII or whatever encoding the text being shown has. I still think it has to do with bad UTC-8 encoding or something like that.
Ideas and suggestions on how to properly encode the API call to please the Amazon gods are welcome and appreciated.
Found the problem: Indy (and Synapse too) adds the port number to the "Host" header line, and I had not realized that extra bit until I watched the headers more closely with Fiddler (thanks, #Graymatter!!!!).
When I change the endpoint to be mws.amazonservices.com:443 (instead of just mws.amazonservices), then my signature is calculated the same way as the AWS server's, and everything works perfectly.

Delphi SOAP server couldn’t handle SOAPAttachemnt after a non-exist method is invoke

I found this problem while I solving another SOAP problem and just post here to see if anyone experienced it or have any recommendation.
The problem:
SOAP server will not process method that receive TSOAPAttachment object after a non-exist method is invoked.
The TSOAPAttachment.SourceStream become inaccessible and TSOAPAttachment.CachFile is empty after the non-exist method is invoked
BorlandSoapAttachment(n) temp file is locked and piling up when the problem happen.
Back to normal after restarting IIS or recycling the App Pool
The steps of create this problem:
Create a simple Soap client/server application with 3 methods initially, one of them should receive and return a TSOAPAttachment, the other 2 can be anything, int or string.
I actually followed this article http://blogs.embarcadero.com/pawelglowacki/2008/12/18/38624 to create the simple soap application, but add a method to receive and return TSOAPAttachment. So my Interface looks like this:
code
TSimpleCalculator = class(TInvokableClass, ISoapCalculator)
private
public
function add(a, b: Integer) : Integer; stdcall;
function subtract(a, b: Integer): Integer; stdcall;
function TestRequest(const Request: TSOAPAttachment): TSOAPAttachment; stdcall;
end;
Create a client of this Soap server to invoke all three methods.
Everything should work happily at this point, ie. the client can invoke all 3 methods and the server could response to all these successfully.
Now, I remove the Subtract function in the server, but still using the original client.
Invoking both Add and TestRequest still complete successfully.
However, after I invoke the non-exist Subtract function, TestRequest will not work anymore but the Add function still work.
The TestRequest will have all the problem I describe above, ie. the server still response to it, but it cant get access to the TSOAPAttachment object.
Any advise is appreciated.
I used soap before, but from Java, not Delphi. But because SOAP is ubiquotus, it does not matter, it has to follow the SOAP specification. The SOAP server should return SOAP error as reply from a SOAP request that does not confront to the published methods. Your Delphi SOAP server should generate SOAP error and your Delphi SOAP client should handle the error properly. You can use a TCP traffic monitor and assign a filter so it only shows a SOAP traffic or use a SOAP traffic monitor. With a monitor you can see the request send to the server (including the message) and the reply as well. You can see the W3C online documentation about the SOAP specification. After all, your SOAP application must follow the spec.

How to send large files from RadPHP server to Delphi XE datasnap server

What is the best method to send a large (<50MB) file from a PHP server (written in RADPHP) to a Delphi Datasnap server (Delphi XE). Because of the connectivity issues I would prefer to use HTTP(S) but this is new territory for me.
The PHP server accepts the file upload from the user's browser OK and can encode it (base_64).
But
a) that puts it into a string which can't be the best way to handle it
b) the DataSnap server crashes with 'Max Line Length Exceeded' on receiving the string
The Datasnap server has 4 components - TDSServer, TDSServerClass, TDSHTTPService, and TDSAuthenticationManager.
The RADPHP server uses a DSRestConnection component.
I'm not very familiar with Datasnap, but couldn't you do something like:
Client uploads file to RadPHP server
now you want to send that to a Datasnap server, what you can do, is send a command with a link to download it something like:
RadPHP: hey, new file for you, here's the link: "http://www.mydomain.com/files/filename.extension"
Datasnap: sends a response, i.e. "OK", and starts downloading, on the server side you can use TIdHTTP for example.
and the implementation could be something like:
procedure DatasnapServerClass.NewFile(const ALink: string);
var
LIDHTTP: TIdHTTP;
begin
// create instance of TIdHTTP, and call the link to
// download the file to your desired local folder
// using ALink as the URL
end;

Resources