IdTCPClient cannot connect through weak internet connections - delphi

I have a simple procedure that uses TIdTCPClient to connect to a server. Its purpose is to try to reach the server and block the thread until the connection is established. It works well with stable internet connections. The problem is, when this client is executed on some computers (all of these computers have slow or unstable internet connection like mobile 3G or just in small distant towns), it always fails to connect with "Connect timed out" exception. At the same time abovementioned computers run browsers, IM clients, etc, and these applications work fine. My server application also shows no sign of new client.
Here is the procedure:
procedure ConnectToServer;
begin
EnterCriticalSection(CS_Connect);
try
while not Client.Connected do
begin
Log('Connection attempt...');
Client.Port := <port goes here>;
Client.IPVersion := Id_IPv4;
Client.Host := '<ip address here>';
Client.ConnectTimeout := 11500;
try
Client.Connect;
except
on E: Exception do
Log('Exception: ' + E.ToString);
end;
if Client.Connected then
begin
Log('Connected after ' + inttostr(attempts) + ' failed attempts');
Client.IOHandler.WriteLn(MSG_HELLO);
attempts := 0;
exit;
end;
Inc(attempts);
Log('Connection attempt failed');
Sleep( min(attempts * 1000, 50000) );
end;
finally
LeaveCriticalSection(CS_Connect);
end;
end;

You are not doing anything wrong (though your use of Connected is redundant). Indy uses the same socket API that other apps use. They all use the same underlying connect() function. So the problem is not with Indy, it is with the OS itself. Since you have a weak signal, it is hit/miss when any given connection will succeed or fail.

Related

Indy IdFTP fails on Active connection

I'm trying to use Indy's IdFTP to send and receive some files via FTP.
function TDatosFTP.TransfiereFTP(Fm: TForm): boolean;
var
TimeoutFTP: integer;
begin
Result := false;
with TIdFTP.Create(Fm) do
try
try
TimeoutFTP := 2000;
Host := Servidor;
Port := 21;
UserName := Usuario;
PassWord := Contra;
Passive := Pasivo;
Connect(true, TimeoutFTP);
if not Connected then
begin
Error := true;
end
else
begin
TransferType := ftASCII;
if Binario then
TransferType := ftBinary;
OnWorkEnd := FinDeTransmision;
if Descargar then
Get(Remoto , Local, True)
else
Put(InterpretarRutaEspecial(Local), Remoto, True);
if Descargar and Borrar then
Delete(Remoto);
Disconnect;
Result := true;
Fm.Hide;
end;
Except on E: Exception do
Mensaje := E.Message;
end;
finally
Free;
end;
if not Result then
ErrorTransmision;
end;
Whenever I try to do a PUT/GET on Active mode I get the following error: EIdProtocolReplyError: 'Failed to establish connection". It works fine on Passive mode.
The thing is that I want to use Indy (used elsewhere in the project) but the previous version of the code, using OverbyteIcsFtpCli works fine both in Active and Passive mode.
This is the code using OverbyteIcsFtpCli:
function TDatosFTP.TransfiereFTP(Fm: TForm): boolean;
begin
with TFtpClient.Create(Fm) do
try
HostName := Servidor;
Port := '21';
UserName := Usuario;
PassWord := Contra;
HostDirName := '';
HostFileName := Origen;
LocalFileName := InterpretarRutaEspecial(Destino);
Binary := Binario;
Passive := Pasivo;
OnRequestDone := FinDeTransmision;
if Descargar then
Result := Receive
else
Result := Transmit;
OnRequestDone := nil;
if Descargar and Borrar then
Delete;
Result := Result and not Error;
Fm.Hide;
if not Result then
ErrorTransmision;
finally
Free;
end;
end;
So I took a look under the hood using wireshark and I found that Indy's FTP is not answering some messages from the server.
This is the file-transmission handshake with OverBytes' FTP:
I've highlighted in yellow the two packets sent between server and client that start the data transmission.
Now let's see what happens with Indy's FTP:
The server is sending the packet to start the file transmission but IdFTP is not answering.
I've seen this question but this two tests where ran in the same computer, same network connection, same firewall, etc. Also this one, but I want the FTP to work both in active and passive modes.
What's happening?
In an Active mode transfer, an FTP server creates an outgoing TCP connection to the receiver.
Your Wireshark captures clearly show that the FTP server in question is creating that transfer connection BEFORE sending a response to the RETR command to let your client know that the connection is proceeding. TFtpClient is accepting that connection before receiving the RETR response. But TIdFTP waits for the RETR response before it will then accept the transfer connection (this also applies to TIdFTP's handling of STOR/STOU/APPE commands, too).
LPortSv.BeginListen; // <-- opens a listening port for transfer
...
SendPort(LPortSv.Binding); // <-- sends the PORT command
...
SendCmd(ACommand, [125, 150, 154]); // <-- sends the RETR command and waits for a response!
...
LPortSv.Listen(ListenTimeout); // <-- accepts the transfer connection
...
Re-reading RFC 959, it says the following:
The passive data transfer process (this may be a user-DTP or a second server-DTP) shall "listen" on the data port prior to sending a transfer request command. The FTP request command determines the direction of the data transfer. The server, upon receiving the transfer request, will initiate the data connection to the port. When the connection is established, the data transfer begins between DTP's, and the server-PI sends a confirming reply to the user-PI.
ICS is asynchronous, so this situation is not a big deal for it to handle. But Indy uses blocking sockets, so TIdFTP will need to be updated to account for this situation, likely by monitoring both command and transfer ports simultaneously so it can act accordingly regardless of the order in which the transfer connection and the command response arrive in.
I have opened a ticket in Indy's issue tracker for this:
#300: TIdFTP fails on Active mode transfer connection with vsFTPd
UPDATE: the fix has been merged into the main code now.

FireDAC "unable to complete network request to host"

The full error text is Remote error: [FireDAC][Phys][FB]Unable to complete network request to host "dataserver16". Error writing data to the connection. Now it seems that others have had this problem then once they sorted it, it went away, but I have the problem sporadically.
My Datasnap ISAPI.dll which contains the FireDAC Firebird connection, is running on an IIS server on a different machine to the one where the database is hosted (dataserver16) but on the same subnet. I know everything is configured correctly, because the application works to expectations about 70% of the time! The other 30% of the time, my Datasnap client receives this error (as passed back from the dll).
IMHO it looks like there is a Network issue. If the Connection is Etablished and you can read and write Data to this connection it seams to be correct.
Have you tried to do a Ping from your Source System to the Target and log that Ping so you can See if the hole Connection to the Server disapears?
Open Commandwindow as Admin and Type:
Ping {TARGET} -t >> c:\ping.log
Than wait until the Error apears and check the Logfile if your Target was available the hole Time.
For more Help we need more Background Information, like Firebird Version or If you are able to reproduce the Error + Source Code how you set up your Connection.
For completeness, I am posting my solution here. Perhaps others will gain benefit from this answer. The solution is to perform retries of the Firebird connection. The way I did it, is every TSQLQuery's BeforeOpen event handler is wired to the same method. This has improved reliability considerably (even if it slowed it down a little). The code for FireDAC is similar. Both DBX and FireDac work equally well here.
const
retrycount = 3;
procedure TServerMethodsDBX.QueryBeforeOpen(DataSet: TDataSet);
begin
TryConnect(TSQLQuery(DataSet).SQLConnection);
// ...
end;
procedure TServerMethodsDBX.TryConnect(SQLConn: TSQLConnection);
var
i: Integer;
Error: String;
begin
i := 0;
SQLConn.Close;
while (not SQLConn.Connected) and (i < retrycount) do
begin
try
SQLConn.Connected := True
except
on e: exception do
begin
Error := Error + ' ' + e.Message;
Sleep(500);
Inc(i);
end;
end;
end;
if i = retrycount then
LogMessage('Tryconnect error: ' + Error);
end;

Network problems while using Indy

function UploadToFTP(file: string ; PathSrv : string): Boolean;
var
server, port, user , password: string;
SR : TSearchRec;
begin
Result := True;
FEventLogger := TEventLogger.Create('Upload FTP');
if file <> '' then
begin
try
server := FServer;
port := FPort;
user := FUserName;
password:= FPassword;
FindFirst(file, faArchive, SR);
try // try except
idftp1.Host:= server;
idftp1.Port := StrToInt(port);
idftp1.Username:= user;
idftp1.Password:= password;
idftp1.Connect();
idftp1.Put(file,PathSrv+SR.Name);
except on E: Exception do begin
Result:= False;
FEventLogger.LogMessage('Exception : ' + E.Message , EVENTLOG_ERROR_TYPE , 0, 2);
WriteToLog('Exception: '+ file+' error message: '+ E.Message);
end;
end;
finally
end;
end;
end;
So I have this function that does an ftp upload to some large files on sometimes slow networks. I've tested it localy and it works ok, but on slow networks i get this eror in 99% of the time.
The specified network name is no longer available.
This is a very strange behavior, becouse the FTP is located on a server that has no disconnecting issues. I also try to watch, and it start the file upload and it does almost all the upload before throwing this error. So for example if I have a 100MB file it does 99MB of the upload then throws the error.
Any ideas what is causing this error or what can I do?
Also from time to time I have an other error
Socket Error # 10054
Connection reset by peer.
To mention,i've tried to upload this files using filezilla and it works, so the problem is somewhere in that code, I might miss something.
Are you using Symantec Endpoint Protection or KIS (Kaspersky Internet Security) ? Take a look here, here and here.
The "The specified network name is no longer available." is caused by Symantec Endpoint Protection 11.0, Symantec has identified this as a known issue.
Btw, in your code don't forget to call .Disconnect() after you're finished uploading the file.

Check remote port access using Delphi - Telnet style

I deploy my application in environments heavily stricken with firewalls. Frequently I find myself using Telnet to check if a port is open and accessible in the network.
Now I would like to implement an equivalent functionality of the command, Telnet [domainname or ip] [port], in Delphi.
Is it adequate that I just attempt to open and close a TCP/IP socket without sending or receiving any data?
Is there any risk that I might crash the arbitrary application/service listening on the other end?
Here's my code:
function IsPortActive(AHost : string; APort : Word):boolean;
var IdTCPClient : TIdTCPClient;
begin
IdTCPClient := TIdTCPClient.Create(nil);
try
try
IdTCPClient.Host := AHost;
IdTCPClient.Port := APort;
IdTCPClient.Connect;
except
//Igonre exceptions
end;
finally
result := IdTCPClient.Connected;
IdTCPClient.Disconnect;
FreeAndNil(IdTCPClient);
end;
end;
If you just want to check whether the port is open, then you can use this:
function IsPortActive(AHost : string; APort : Word): boolean;
var
IdTCPClient : TIdTCPClient;
begin
Result := False;
try
IdTCPClient := TIdTCPClient.Create(nil);
try
IdTCPClient.Host := AHost;
IdTCPClient.Port := APort;
IdTCPClient.Connect;
Result := True;
finally
IdTCPClient.Free;
end;
except
//Ignore exceptions
end;
end;
But that only tells you if any server app has opened the port. If you want to make sure that YOUR server app opened the port, then you will have to actually communicate with the server and make sure its responses are what you are expecting. For this reason, many common server protocols provide an initial greeting so clients can identify the type of server they are connected to. You might consider adding a similar greeting to your server, if you are at liberty to make changes to your communication protocol.
Simply opening a connection to the server does not impose any risk of crashing the server, all it does is momentarily occupy a slot in the server's client list. However, if you actually send data to the server, and the server app you are connected to is not your app, then you do run a small risk if the server cannot handle arbitrary data that does not conform it its expected protocol. But that is pretty rare. Sending a small command is not uncommon and usually pretty safe, you will either get back a reply (which may be in a format that does not conform to your protocol, so just assume failure), or you may not get any reply at all (like if the server is waiting for more data, or simply is not designed to return a reply) in which case you can simply time out the reading and assume failure.

How can I get the Winsock error code for a dbExpress connect error?

In case of a connection problem, the dbExpress driver throws a TDBXError but does not include a socket error code. The message is simply:
Unable to complete network request to host "exampledb.local".
Failed to establish a connection
Is there a way to retrieve the underlying socket error when this type of exception occurs?
The stack trace is:
main thread ($5934):
0061aa59 +051 example.exe DBXCommon 447 +0 TDBXContext.Error
00817f14 +10c example.exe DBXDynalink 796 +21 TDBXMethodTable.RaiseError
00818553 +013 example.exe DBXDynalink 949 +1 TDBXDynalinkConnection.CheckResult
00818744 +050 example.exe DBXDynalink 1048 +4 TDBXDynalinkConnection.DerivedOpen
0061750f +007 example.exe DBXCommon 447 +0 TDBXConnection.Open
00612fed +0f5 example.exe DBXCommon 447 +0 TDBXConnectionFactory.GetConnection
00612ef1 +005 example.exe DBXCommon 447 +0 TDBXConnectionFactory.GetConnection
0062c08f +26f example.exe SqlExpr TSQLConnection.DoConnect
005d9019 +039 example.exe DB TCustomConnection.SetConnected
005d8fd4 +004 example.exe DB TCustomConnection.Open
0062b98f +01b example.exe SqlExpr TSQLConnection.CheckConnection
0062ebdf +01f example.exe SqlExpr TCustomSQLDataSet.CheckConnection
0062efe4 +04c example.exe SqlExpr TCustomSQLDataSet.OpenCursor
005e91d5 +055 example.exe DB TDataSet.SetActive
005e901c +004 example.exe DB TDataSet.Open
(Delphi XE2 and 'gds32.dll' 10.0.1.335 (ansi) is used in this answer)
dbExpress is a high level framework that delegates provider specific operations to its drivers. All of these provider specific drivers expose the same basic common functionality, hence it is not possible to retrieve specific information which falls outside this common functionality.
Regarding error reporting, the drivers export two functions. Be it the mysql or the mssql or any other driver, these functions are DBXBase_GetErrorMessageLength and DBXBase_GetErrorMessage. The drivers get most of what went wrong from client libraries and report that information to the framework through these functions. Beyond what have already reported, it is not possible to get any more specific detail about what went wrong about connecting to a database server or any other operation.
So it is up to the client libraries to include winsock error information or not. In the case of the interbase driver, which the error included in the question is from, this information is already included (when there is one).
Here is an example case, an attempt to connect to an existing server, and to a port that is listened but not by the database server.
Connection := TSQLConnection.Create(nil);
Connection.DriverName := 'Interbase';
Connection.Params.Values['Database'] := 'existingserver/80:database';
try
Connection.Open;
except
on E: TDBxError do begin
writeln(E.Message + sLineBreak);
Here is the output:
Unable to complete network request to host "existingserver:80".
Failed to establish a connection.
As you notice this is exactly the same error message in the question. Note that there is no winsock error in the text. That is because there is no winsock error, as long as the network protocol is concerned, the connection is actually successful.
Here is an attempt to an existing server, but to a non-listened port.
Connection := TSQLConnection.Create(nil);
Connection.DriverName := 'Interbase';
Connection.Params.Values['Database'] := 'existingserver/81:database';
try
Connection.Open;
except
on E: TDBxError do begin
writeln(E.Message + sLineBreak);
Here is the returned error:
Unable to complete network request to host "existingserver:81".
Failed to establish a connection.
unknown Win32 error 10060
This time the connection fails since there is no listener for the specific port. I have no idea about why the client library fails to resolve 10060, but this is your winsock error.
Case for localhost, non-listened port:
Unable to complete network request to host "localhost:3051".
Failed to establish a connection.
No connection could be made because the target machine actively refused it.
Here we have a 10061.
When an attempt to connect to a non-resolvable host is made, gds32.dll does not report any api error. I don't know how the library resolves a host or why it doesn't include error code, but the error message is verbose:
Unable to complete network request to host "nonexistingserver".
Failed to locate host machine.
The specified name was not found in the hosts file or Domain Name Services.
If we were to directly use the client library, we could get only the api error. Examine the following program where a connection attempt is made to an existing server and a closed port.
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
system.sysutils,
data.dbxcommon,
data.sqlexpr,
data.dbxinterbase;
function isc_attach_database(status_vector: PLongint; db_name_length: Smallint;
db_name: PAnsiChar; db_handle: PPointer; parm_buffer_length: Smallint;
parm_buffer: PAnsiChar): Longint; stdcall; external 'gds32.dll';
function isc_interprete(buffer: PAnsiChar; var status_vector: Pointer): Longint;
stdcall; external 'gds32.dll';
var
Connection: TSQLConnection;
StatusVector: array[0..19] of Longint;
Handle: PPointer;
Error: array[0..255] of AnsiChar;
IntrStatus: Pointer;
s: string;
begin
try
Connection := TSQLConnection.Create(nil);
Connection.DriverName := 'Interbase';
Connection.Params.Values['Database'] := 'server/3051:database'; // closed port
try
Connection.Open;
except
on E: TDBxError do begin
writeln(E.Message + sLineBreak);
if E.ErrorCode = TDBXErrorCodes.ConnectionFailed then begin
Handle := nil;
if isc_attach_database(#StatusVector, 0,
PAnsiChar(AnsiString(Connection.Params.Values['Database'])),
#Handle, 0, nil) <> 0 then begin
IntrStatus := #StatusVector;
s := '';
while isc_interprete(Error, IntrStatus) <> 0 do begin
s := s + AnsiString(Error) + sLineBreak;
if PLongint(IntrStatus)^ = 17 then // isc_arg_win32
s := s + ' --below is an api error--' + sLineBreak +
Format('%d: %s', [PLongint(Longint(IntrStatus) + 4)^,
SysErrorMessage(PLongint(Longint(IntrStatus) + 4)^)]) +
sLineBreak;
end;
Writeln(s);
end;
end else
raise;
end;
end;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
readln;
end.
Which outputs:
Unable to complete network request to host "sever:3051".
Failed to establish a connection.
unknown Win32 error 10060
Unable to complete network request to host "server:3051".
Failed to establish a connection.
--below is an api error--
10060: A connection attempt failed because the connected party did not properly
respond after a period of time, or established connection failed because connect
ed host has failed to respond
unknown Win32 error 10060
The first paragraph is what dbx reports. The second paragraph is what we get from 'gds32.dll' including the injection of api error code and text, otherwise they are the same.
The above is a crude demonstration, for proper typing use 'interbase.h'. And for details about picking a possible api error, see "Parsing the Status Vector", or in general "Handling Error Conditions".
In any case, as can be seen, being able to get specific information entirely depends on the client library that dbx uses to connect to the database server.
For the general case, to get winsock error information independently from the database server being used, what you can do is to attempt to connect a socket to the server before trying to open a database connection, and only if this is successful close your test connection and then proceed attaching to the database. You can use any library or bare api to do this.
Here is a simple example:
function TestConnect(server: string; port: Integer): Boolean;
procedure WinsockError(step: string);
begin
raise Exception.Create(Format('"%s" fail. %d: %s:',
[step, WSAGetLastError, SysErrorMessage(WSAGetLastError)]));
end;
var
Error: Integer;
Data: TWSAData;
Socket: TSocket;
SockAddr: TSockAddrIn;
Host: PHostEnt;
begin
Result := False;
Error := WSAStartup(MakeWord(1, 1), Data);
if Error = 0 then begin
try
Socket := winsock.socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if Socket <> INVALID_SOCKET then begin
try
Host := gethostbyname(PAnsiChar(AnsiString(server)));
if Host <> nil then begin
SockAddr.sin_family := AF_INET;
SockAddr.sin_addr.S_addr := Longint(PLongint(Host^.h_addr_list^)^);
SockAddr.sin_port := htons(port);
if connect(Socket, SockAddr, SizeOf(SockAddr)) <> 0 then
WinsockError('connect')
else
Result := True;
end else
WinsockError('gethostbyname');
finally
closesocket(Socket);
end;
end else
WinsockError('socket');
finally
WSACleanup;
end;
end else
raise Exception.Create('winsock initialization fail');
end;
You can use something like this like:
if TestConnect('server', 3050) then
//

Resources