IdHTTPServer and IdHTTP with Encoding UTF8 - delphi

I am testing a localhost server using TIdHTTPServer and TIdHTTP. I am having problems with encoding UTF8 data.
client side:
procedure TForm1.SpeedButton1Click(Sender: TObject);
var
res: string;
begin
res:=IdHTTP1.Get('http://localhost/?msg=đi chơi thôi');
Memo1.Lines.Add(res);
end;
Server side:
procedure TForm1.OnCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
Memo1.Lines.Add(ARequestInfo.Params.Values['msg']); // ?i ch?i th?i
AResponseInfo.CharSet := 'utf-8';
AResponseInfo.ContentText := 'chào các bạn'; // chào các b?n
end;
I want to send đi chơi thôi and receive chào các bạn. But the server receives ?i ch?i th?i and the client receives chào các b?n.
Can anyone help me?

TIdHTTP transmits the URL exactly as you give it, but http://localhost/?msg=đi chơi thôi is not a valid URL that can be transmitted as-is, as URLs can only contain ASCII characters. Unreserved ASCII characters can be used as-is, but reserved and non-ASCII characters MUST be charset-encoded into bytes and then those bytes must be url-encoded in %HH format, eg:
IdHTTP1.Get('http://localhost/?msg=%C4%91i%20ch%C6%A1i%20th%C3%B4i');
You must ensure you pass only valid url-encoded URLs to TIdHTTP.
In this example, the URL is hard-coded, but if you need something more dynamic then use the TIdURI class, eg:
IdHTTP1.Get('http://localhost/?msg=' + TIdURI.ParamsEncode('đi chơi thôi'));
TIdHTTPServer will then decode the parameter data as you are expecting. Both TIdURI and TIdHTTPServer use UTF-8 by default.
When sending a response, you are only setting a CharSet, but you are not setting a ContentType. So TIdHTTPServer will set the ContentType to 'text/html; charset=ISO-8859-1', overwriting your CharSet. You need to explicitly set the ContentType yourself so you can specify a custom CharSet, eg:
AResponseInfo.ContentType := 'text/plain';
AResponseInfo.CharSet := 'utf-8';
AResponseInfo.ContentText := 'chào các bạn';
Or:
AResponseInfo.ContentType := 'text/plain; charset=utf-8';
AResponseInfo.ContentText := 'chào các bạn';
On a side note, TIdHTTPServer is a multi-threaded component. The OnCommand... events are fired in the context of a worker thread, not the main UI thread. So accessing Memo1 directly like you are is not thread-safe. You MUST synchronize with the main UI thread in order to access UI controls safely, eg:
procedure TForm1.OnCommandGet(AContext: TIdContext; ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
msg: string;
begin
msg := ARequestInfo.Params.Values['msg'];
TThread.Synchronize(nil,
procedure
begin
Memo1.Lines.Add(msg);
end
);
...
end;

Related

Indy Http Server delivers Javascript with Syntax errors

I am trying to use Indy to serve Javascript (deploying a Swagger UI to render API documentation).
procedure TfmMain.SendJavaScriptFileResponse(AResponseInfo: TIdHTTPResponseInfo; AFileName: String);
begin
AResponseInfo.ContentType := 'application/javascript';
AResponseInfo.CharSet := 'utf-8';
var LFileContents := TStringList.Create;
try
LFileContents.LoadFromFile(AFileName);
AResponseInfo.ContentText := LFileContents.Text;
finally
LFileContents.Free;
end;
end;
When the browser receives the Javascript and attempts to run it, I get a syntax error:
Uncaught SyntaxError: illegal character U+20AC
The respoinsde headers received from the Indy IdHttpServer look like so:
HTTP/1.1 200 OK
Connection: close
Content-Encoding: utf-8
Content-Type: application/javascript; charset=utf-8
Content-Length: 1063786
Date: Sun, 05 Feb 2023 20:45:56 GMT
However, when I serve the exact same Javascript files via my hosted website, the Javascript runs fine in the browser with no errors.
Is there a setting or character set I need to use when sending Javascript files using the Indy HTTP server?
You are loading the Javascript from a file into a string, and then you are sending that string to the client. That requires 2 data conversions at runtime - from the file's encoding to UTF-16 in memory, and from UTF-16 to the specified AResponseInfo.Charset on the data transmission to the client. Either one of those conversions can fail if you are not careful.
In memory, a string in Delphi 2009+ is always UTF-16 encoded, but you are not specifying the file's encoding when loading the file into the TStringList. So, if the file uses an encoding other than ASCII (say, UTF-8), does not have a BOM, and contains any non-ASCII characters (say, the Euro sign €), then TStringList WILL NOT decode the file into UTF-16 correctly. In which case, you MUST specify the file's actual encoding, eg:
procedure TfmMain.SendJavaScriptFileResponse(
AResponseInfo: TIdHTTPResponseInfo;
const AFileName: String);
begin
AResponseInfo.ContentType := 'application/javascript';
AResponseInfo.CharSet := 'utf-8';
var LFileContents := TStringList.Create;
try
LFileContents.LoadFromFile(AFileName, TEncoding.UTF8); // <-- HERE
AResponseInfo.ContentText := LFileContents.Text;
finally
LFileContents.Free;
end;
end;
Another option is to send the actual file itself, without having to load and decode it into memory first, eg:
procedure TfmMain.SendJavaScriptFileResponse(
AContext: TIdContext;
AResponseInfo: TIdHTTPResponseInfo;
const AFileName: String);
begin
AResponseInfo.ContentType := 'application/javascript';
AResponseInfo.CharSet := 'utf-8';
AResponseInfo.ServeFile(AContext, AFileName);
end;
Either way, utf-8 is not a valid value for the HTTP Content-Encoding header. Indy does not assign any value to that header by default, so you must be assigning it manually. Don't do that in this case.

TIdHTTPServer + TIdServerIOHandlerSSLOpenSSL = unstable work

I placed TIdHTTPServer and TIdServerIOHandlerSSLOpenSSL on the form, set sertificate file path for the TIdServerIOHandlerSSLOpenSSL
In the TIdHTTPServer onCommandGet event I send answer in this way:
FullResponce:=RespHeaders+#13#10+RespData;
//dump for debug
FileHandle := CreateFileW('c:\lastresp.txt', GENERIC_WRITE, FILE_SHARE_READ, nil, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
if FileHandle <> INVALID_HANDLE_VALUE then
begin
try
WriteFile(FileHandle, FullResponce[1], length(FullResponce), LongWord(ReadRes), nil);
finally
CloseHandle(FileHandle);
end;
end;
//response
AContext.Connection.IOHandler.WriteDirect(#FullResponce[1],length(FullResponce),0);
exit;
When I'm trying to open address '127.0.0.1:433' in the Internet Explorer, sometimes it opens page, sometimes says "unable to display page"
There is no any pattern or logic in this, I just refresh the page in the explorer 10 times, and for example, attempts 1,2,4,6,7,9,10 had open the page, and the rest ended with an error
Dupm of the answer in the lastresp.txt is always correct and always the same
When the error occurs, IE in the network console shows that he received 0 bytes
Any ideas how to fix this?
Bugs in Indy?
Update
Solved, problem was not in code but in sniffer app used to control traffic(
How you have implemented the OnCommandGet event is not the correct way to send a response to the client.
First off, you are not supposed to use TIdIOHandler.WriteDirect() directly at all. But if you do, you must pass it a TIdBytes, ie a dynamic array of bytes, not a string.
But more importantly, to send an HTTP response to the client, the correct way is to populate the AResponseInfo parameter and let TIdHTTPServer handle the actual socket writing for you.
For example:
procedure TMyForm.IdHTTPServer1CommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
...
AResponseInfo.ResponseCode := 200;
AResponseInfo.ContentText := RespData;
// set other AResponseInfo properties as needed, like ContentType, etc...
end;
When the OnCommandGet handler exits, TIdHTTPServer will send the contents of the AResponseInfo to the client, if it has not already been sent.
Also, your use of WriteFile() is incorrect if you are using Delphi 2009+ and FullResponce is a normal string (ie, a UnicodeString). It would need to look like this instead:
WriteFile(FileHandle, PChar(FullResponce)^, Length(FullResponce) * SizeOf(Char), LongWord(ReadRes), nil);
Consider using TFile.WriteAllText() instead:
TFile.WriteAllText('c:\lastresp.txt', FullResponce);
However, another way to add logging to your TIdHTTPServer code is to instead assign a TIdLogFile component to the AContext.Connection.IOHandler.Intercept property and let Indy itself log exactly what it sends to the client, eg:
uses
..., IdLogFile;
procedure TMyForm.IdHTTPServer1CommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
Log: TIdLogFile;
begin
Log := TIdLogFile.Create(nil);
try
Log.Filename := 'c:\lastresp.txt';
Log.LogTime := False;
Log.ReplaceCRLF := False;
Log.Active := True;
AContext.Connection.IOHandler.Intercept := Log;
...
AResponseInfo.ResponseCode := 200;
AResponseInfo.ContentText := RespData;
// set other AResponse properties as needed, like ContentType, etc...
...
AResponse.WriteContent;
finally
Log.Free;
end;
end;

Changing SSL certificates on the fly in IdHTTPServer

I'm developing a MITM proxy with SSL support in delphi 10.3, using indy. I use IdHttpServer component, which does work in CommandOther event. I managed to make it to decrypt and dump data on the fly, and reencrypt and send it to browser, but I need to change idhttpserver certificate for each domain. I can generate them and install my own CA, but i cant find a way to change them while my proxy is working. I will greatly appreciate if someone will show me how!
procedure TForm3.IdHTTPServer1CommandOther(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
S: string;
LClient: TIdtcpClient;
newsize:int64;
LBuf: TIdBytes;
Len: Integer;
var response:integer;
s3: TStringDynArray;
var cmd:string;
bytes:tidbytes;
oldstr,newstr:string;
ResponseCode, ResponseText: string;
Size: Int64;
ssl:tIdServerIOHandlerSSLopenssl;
begin
if not TextIsSame(ARequestInfo.Command, 'CONNECT') then Exit;
LClient := TIdtcpClient.Create(nil);
try
S := ARequestInfo.URI;
LClient.Host := Fetch(S, ':', True);
LClient.Port := StrToIntDef(S, 443);
LClient.IOHandler := TIdSSLIOHandlerSocketOpenSSL.Create(LClient);
LClient.ConnectTimeout := 5000;
// connect and activate SSL between this proxy and the target server
LClient.Connect;
try
AResponseInfo.ResponseNo := 200;
AResponseInfo.ResponseText := 'Connection established';
AResponseInfo.WriteHeader;
// activate SSL between this proxy and the client
TIdSSLIOHandlerSocketOpenSSL(AContext.Connection.Socket).PassThrough:=false;
// pass data between AContext.Connection.IOHandler and LClient.IOHandler
//as needed.
// received data will be decrypted, and sent data will be encryted...
while AContext.Connection.Connected and lclient.Connected do
begin
//mitm traffic modification routine
end;
finally
LClient.Disconnect;
end;
finally
LClient.Free;
end;
end;
This is cert switching code:
procedure TForm3.IdHTTPServer1Connect(AContext: TIdContext);
var
SSL: TIdSSLIOHandlerSocketOpenSSL;
begin
if AContext.Connection.Socket.Binding.Port = 443 then
begin
sslh:=tIdSSLIOHandlerSocketOpenSSL(AContext.Connection.IOHandler);
sslh.SSLOptions.CertFile:='Certificate.pem';
sslh.SSLOptions.keyfile:='PrivateKey.pem';
sslh.SSLOptions.RootCertFile:='certificateAuthorityCertificate.pem';
sslh.SSLOptions.SSLVersions:=[sslvSSLv23];
sslh.ssloptions.mode:=sslmBoth;
sslh.OnGetPassword:= IdServerIOHandlerSSLOpenSSL1GetPassword;
sslh.PassThrough:=false;
TIdSSLIOHandlerSocketOpenSSL(AContext.Connection).PassThrough:=false;
//memo2.Text:=AContext.Connection.IOHandler.ReadLn();
end;
end;
On a form I have a tidhttpserver and TIdServerIOHandlerSSLOpenSSL as iohandler for it.
Assign on OnQuerySSLPort event handler that unconditionally sets the VUseSSL parameter to False regardless of the APort requested. Then, in the OnConnect event, if the AContext.Connection.Socket.Binding.Port property is 443 (or whatever port you want to use HTTPS on), you can typecast the AContext.Connection.IOHandler property to TIdSSLIOHandlerSocketBase (or descendant, like TIdSSLIOHandlerSocketOpenSSL if using OpenSSL), configure its certificates as needed, and then set its PassThrough property to False to complete the SSL/TLS handshake.

Authorization failure TIdHTTP over HTTPS when password is russian

I try to test my webservice with the TIdHTTP (Indy 10.6.0 and Delphi XE5) by this code:
GIdDefaultTextEncoding := encUTF8;
HTTP.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
Http.Request.UserName := AUser;
Http.Request.Password := APass;
Http.Request.Accept := 'text/javascript';
Http.Request.ContentType := 'application/json';
Http.Request.ContentEncoding := 'utf-8';
Http.Request.URL := 'https://sameService';
Http.MaxAuthRetries := 1;
Http.Request.BasicAuthentication := True;
TIdSSLIOHandlerSocketOpenSSL(HTTP.IOHandler).SSLOptions.Method := sslvSSLv3;
HTTP.HandleRedirects := True;
"AUser" and "APass" in UTF-8. When "APass" have same Russian chars I can't login.
By "HTTP Analyze" I see:
...
Authorization: Basic cDh1c2VyOj8/Pz8/PzEyMw==
Decode from Base 64 (base64decode.org) we can see:
p8user:??????123
Why DefStringEncoding not work ?
TIdHTTP's authentication system has no concept of TIdIOHandler or its DefStringEncoding property.
Internally, TIdBasicAuthentication uses TIdEncoderMIME.Encode(), but without specifying any encoding. TIdEncoder.Encode() defaults to 8bit encoding, and thus is not affected by GIdDefaultTextEncoding.
If you need to send a UTF-8 encoded password with BASIC authentication, you will have to encode the UTF-8 data manually and store the resulting octets into a string, then the 8bit encoder can process the octets as-is, eg:
Http.Request.Password := BytesToStringRaw(IndyTextEncoding_UTF8.GetBytes(APass));
On the other hand, Indy's DIGEST authentication, for instance, uses TIdHashMessageDigest5.HashStringAsHex(), and TIdHash.HashString() does not default to any specific encoding, it depends on GIdDefaultTextEncoding.
So, you have to be careful about how you encode passwords, based on which authentications you use. To account for the discrepency, what you could try is not encode TIdHTTP.Request.Password itself, but instead encode the password inside the TIdHTTP.OnAuthorization event instead when BASIC authentication is being used, eg:
Http.Request.Password := APass;
...
procedure TMyForm.HttpAuthorization(Sender: TObject;
Authentication: TIdAuthentication; var Handled: Boolean);
begin
if Authentication is TIdBasicAuthentication then
begin
Authentication.Password := BytesToStringRaw(IndyTextEncoding_UTF8.GetBytes(TheDesiredPasswordHere));
Handled := True;
end;
end;
UPDATE:
Internally, TIdBasicAuthentication uses TIdEncoderMIME.Encode(), but without specifying any encoding.
That last part is no longer true. TIdBasicAuthentication was updated in 2016 to now pass an encoding to TIdEncoderMIME.Encode(). When an HTTP server asks for BASIC authentication, TIdBasicAuthentication now checks if the server's WWW-Authenticate header includes one of the following attributes: charset, accept-charset, encoding, or enc (in that order). If one is found, the specified charset is passed to Encode(), otherwise ISO-8859-1 is used (there is a TODO in the code to use UTF-8 if the username or password contain any characters that do not exist in ISO-8859-1).
If you want to ensure that UTF-8 is used in BASIC authentication, you are better off setting Request.BasicAuthentication to False and using the Request.CustomHeaders to supply your own Authorization header, eg:
Http.Request.BasicAuthentication := False;
Http.Request.CustomHeaders.Values['Authorization'] := 'Basic ' + TIdEncoderMIME.EncodeString(AUser + ':' + APass, IndyTextEncoding_UTF8);
Alternatively, you might be able to just get away with updating the protected TIdBasicAuthentication.FCharset member inside of the TIdHTTP.OnAuthorization event (which is fired after the server's WWW-Authenticate header has been parsed), eg:
Http.Request.Password := APass;
...
type
TIdBasicAuthenticationAccess = class(TIdBasicAuthentication)
end;
procedure TMyForm.HttpAuthorization(Sender: TObject;
Authentication: TIdAuthentication; var Handled: Boolean);
begin
if Authentication is TIdBasicAuthentication then
begin
TIdBasicAuthenticationAccess(Authentication).FCharset := 'utf-8';
Authentication.Password := TheDesiredPasswordHere;
Handled := True;
end;
end;

How to handle TIdHTTPServer with TIdMultiPartFormDataStream

Hi i need help on how to retrieved the parameters and data using IdHttpServer from indy.
many of my application uses TIdMultiPartFormDataStream to send data over the php. I would like to use the TIdHTTPServer to verify parameters for some reason and forward the request to its destination.
i created a short example for you to see.
uses
IdContext, IdMultipartFormData;
// Server Side------------------------------------------------
IdHTTPServer1.Defaultport := 88;
IdHTTPServer1.active := True;
procedure TForm1.IdHTTPServer1CommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
// the request will be pass through its destination by POST/GET
// and send the result back to the client apps.
AResponseInfo.ContentText := ARequestInfo.Params.Text;
end;
// Client Side------------------------------------------------
// This will work using the standard Post or Get
procedure TForm1.btnPost1Click(Sender: TObject);
var
sl: TStringList;
res: String;
begin
sl := TStringList.Create;
try
sl.Add('Param1=Data1');
sl.Add('Param2=Data1');
sl.Add('Param3=Data2');
sl.Add('Param4=Data3');
res := IdHTTP1.Post('http://localhost:88/some.php', sl);
ShowMessage(res);
finally
sl.Free;
end;
end;
//how can i get the parameters and value for this code in my IdHttpServer
procedure TForm1.btnPost2Click(Sender: TObject);
var
mfd: TIdMultiPartFormDataStream;
res: String;
begin
mfd := TIdMultiPartFormDataStream.Create;
try
mfd.AddFormField('Param1', 'Data1');
mfd.AddFormField('Param2', 'Data1');
mfd.AddFormField('Param3', 'Data2');
mfd.AddFormField('Param4', 'Data3');
res := IdHTTP1.Post('http://localhost:88/some.php', mfd);
ShowMessage(res);
finally
mfd.Free;
end;
end;
and how would i know if the Client apps pass a TIdMultiPartFormDataStream type of parameter?
This has been asked and answered many times before in the Embarcadero and Indy forums. Please search through their archives, as well as other archives, like Google Groups, to find code examples.
In a nutshell, when the TIdHTTPServer.OnCommandGet event is triggered, if the AResponseInfo.ContentType property says multipart/form-data (the version of TIdHTTP.Post() you are using will send application/x-www-form-urlencoded instead), the AResponseInfo.PostStream property will contain the raw MIME data that the client posted. You can use the TIdMessageDecoderMIME class to parse it. However, that class was never intended to be used server-side, so it is not very intuitive to use, but it is possible nontheless.
In Indy 11, I am planning on implementing native multipart/form-data parsing directly into TIdHTTPServer itself, but there is no ETA on that yet as we have not started work on Indy 11 yet.

Resources