I intent to block certain clients from connecting to my server. I have the following code segments:
procedure TMain.WebServiceConnect(AContext:TIdContext);
Var
IP:String;
begin
try
IP:=AContext.Binding.PeerIP;
if Allowlist.IndexOf(IP)=-1 then Begin;
if Blocklist.IndexOf(IP)>-1 then Begin;
LogWrite('Blocking IP "'+IP+'"!',Detailed);
LogRequest(IP,'*BLOCKED*');
AContext.Connection.Disconnect;
End;
End;
except
ON E:Exception do
LogWrite('"'+E.Message+'" while connection-check on blocklist',Detailed);
end;
end;
procedure TMain.WebServiceReceive(AContext:TIdContext; ARequestInfo:TIdHTTPRequestInfo; AResponseInfo:TIdHTTPResponseInfo);
begin
try
try
LogWrite('Handling connection from '+ARequestInfo.RemoteIP,Debug);
AResponseInfo.ResponseNo:=401;
ProcessWebRequest(AContext,ARequestInfo,AResponseInfo);
LogWrite('Closing connection to '+ARequestInfo.RemoteIP,Debug);
except
ON E:Exception do
LogWrite('"'+E.Message+'" while handling request!',verbose,FALSE);
End;
finally
End;
End;
But I see both events trigger from blocked IPs! I do see the "blocked" logline, but it seems the AContext.Connection.Disconnect doesnt act as expected.
What is wrong?
I see no possible way that the server's OnCommand... events can be fired for a given client connection if the OnConnect event has closed that connection. The server would fail to read the client's request data to populate the TIdRequestInfo object before firing the OnCommand... events.
So, either you are not actually calling Disconnect() like you are expecting, or you are misdiagnosing the issue.
For instance, in your log messages, try including the AContext.Binding.PeerPort, or even the AContext.Binding.Handle, in addition to the PeerIP to make sure the log messages actually belong to the same client connection. PeerIP alone is not adequate to uniquely identify individual connections.
That being said, in the unlikely situation that Disconnect() is actually not closing the connection, you can force a disconnect by raising an exception, such as via SysUtils.Abort(), so that the server will terminate the calling thread and close the socket.
Also, speaking of exceptions, you should re-raise any exception that you catch with except (or, at least, any Indy exception derived from EIdException), let the server handle it.
Try something more like this:
procedure TMain.WebServiceConnect(AContext:TIdContext);
var
IP: String;
Port: TIdPort;
begin
try
IP := AContext.Binding.PeerIP;
Port := AContext.Binding.PeerPort;
if Allowlist.IndexOf(IP) = -1 then begin
if Blocklist.IndexOf(IP) > -1 then begin
LogWrite('Blocking IP "' + IP + ':' + IntToStr(Port) + '"!', Detailed);
LogRequest(IP, '*BLOCKED*');
AContext.Connection.Disconnect;
SysUtils.Abort;
end;
end;
except
on E: Exception do begin
if not (E is EAbort) then
LogWrite('"' + E.Message + '" while connection-check on blocklist', Detailed);
raise;
end;
end;
end;
procedure TMain.WebServiceReceive(AContext: TIdContext; ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
IP: String;
Port: TIdPort;
begin
try
IP := AContext.Binding.PeerIP;
Port := AContext.Binding.PeerPort;
LogWrite('Handling connection from ' + IP + ':' + IntToStr(Port), Debug);
AResponseInfo.ResponseNo := 401;
ProcessWebRequest(AContext, ARequestInfo, AResponseInfo);
LogWrite('Closing connection to ' + IP + ':' + IntToStr(Port), Debug);
except
on E: Exception do begin
LogWrite('"' + E.Message + '" while handling request!', verbose, FALSE);
raise;
end;
end;
end;
Related
I have a simple webserver written in delphi which fetchs data from the DB and retrieve in JSON.
There are 20 endpoints, I will post one of them:
procedure TWM.WMactGruposGetAction(Sender: TObject; Request: TWebRequest;
Response: TWebResponse; var Handled: Boolean);
var
...
begin
// Get query fields
...
// Configurar resposta
Response.ContentType := APPLICATION_JSON + '; ' + CHARSET_UTF8;
// Look for grupos
qryGrupos := TFDQuery.Create(nil);
with qryGrupos do
begin
Connection := dmDados.GetConnection(NomeDB); // Open the
Active := False;
SQL.Clear;
Open('SELECT * FROM ' + T_PESSOAS_GRUPO +
' WHERE ' + C_ID_LOTEAMENTO + ' = ' + IDLoteamento);
if qryGrupos.RecordCount > 0 then
JsonArray := TJSONArray.Create;
try
First;
while not Eof do
begin
JsonObject := TJSONObject.Create;
CapturarCamposGrupos(JsonObject, qryGrupos);
JsonArray.AddElement(JsonObject);
Next;
end;
finally
Response.Content := JsonArray.ToString;
CloseQuery(qryGrupos); // Close and nil the query
dmDados.CloseConnection(NomeDB); // Close the TFDConnection
JsonArray.DisposeOf;
end;
end;
end;
function TdmDados.GetConnection(DBName: string): TFDConnection;
begin
FDConnection.Open();
Result := FDConnection
end;
This basically does the following:
Opens the TFDConnection
Opens the TFDQuery;
Closes the TFDQuery;
Closes the TFDConnection;
Note: The TFDConnection is created at design time when the application starts, so every API call uses the same TFDConnection;
Problem
The problem is that in my flutter app I call three endpoints at the same time (they pretty much do the same thing as this grupos one).
When I call each endpoint at a time it works fine, but when I go to this page that calls three endpoints at the same time. The webserver brokes.
The error messages differ but they were things like:
Lost connection to MySQL server
Access violation address at ...
Cannot connect to MySQL
And then the server never works more for any endpoint.
I feel like somehow the TFDConnection lost the connection and can not get it again. Any help?
I need to write a simple chat program that will be used by some customers. Basically, there are a lot of clients connected to a server and they chat together. The server works:
Here the code if needed:
//CONNECT TO THE SERVER
procedure TFormServer.ButtonStartClick(Sender: TObject);
begin
if not TCPServer.Active then
begin
try
TCPServer.DefaultPort := 8002;
TCPServer.Bindings[0].IP := LIP.Text;
TCPServer.Bindings[0].Port := StrToInt(LPort.Text);
TCPServer.MaxConnections := 5;
TCPServer.Active := true;
Memo1.Lines.Add(TimeNow + 'Server started.');
except
on E: Exception do
Memo1.Lines.Add(sLineBreak + ' ====== INTERNAL ERROR ====== ' +
sLineBreak + ' > ' + E.Message + sLineBreak);
end;
end;
end;
//DISCONNECT
procedure TFormServer.ButtonStopClick(Sender: TObject);
begin
if TCPServer.Active then
begin
TCPServer.Active := false;
Memo1.Lines.Add(TimeNow + 'Server stopped.');
end;
end;
//IF CLOSE THE APP DONT FORGET TO CLOSE SERVER!!
procedure TFormServer.FormClose(Sender: TObject; var Action: TCloseAction);
begin
ButtonStopClick(Self);
end;
procedure TFormServer.FormCreate(Sender: TObject);
begin
FClients := 0;
end;
//When a client connects I write a log
procedure TFormServer.TCPServerConnect(AContext: TIdContext);
begin
Inc(FClients);
TThread.Synchronize(nil, procedure
begin
LabelCount.Text := 'Connected sockets: ' + FClients.ToString;
Memo1.Lines.Add(TimeNow + ' Client connected # ' + AContext.Binding.IP + ':' + AContext.Binding.Port.ToString);
end);
end;
//Same, when a client disconnects I log it
procedure TFormServer.TCPServerDisconnect(AContext: TIdContext);
begin
Dec(FClients);
TThread.Synchronize(nil, procedure
begin
LabelCount.Text := 'Connected sockets: ' + FClients.ToString;
Memo1.Lines.Add(TimeNow + ' Client disconnected');
end);
end;
//WHAT I DO HERE:
//I receive a message from the client and then I send this message to EVERYONE that is connected here. It is a global chat
procedure TFormServer.TCPServerExecute(AContext: TIdContext);
var
txt: string;
begin
txt := AContext.Connection.IOHandler.ReadLn();
AContext.Connection.IOHandler.WriteLn(txt);
TThread.Synchronize(nil, procedure
begin
Memo1.Lines.Add(TimeNow + txt);
end);
end;
The sever code is very easy and minimal but it does what I need. This is the client instead:
Here there is the code, very simple:
//CONNECT TO THE SERVER
procedure TFormClient.ConnectClick(Sender: TObject);
begin
if Length(Username.Text) < 4 then
begin
Memo1.Lines.Clear;
Memo1.Lines.Add('ERROR: Username must contain at least 4 characters');
Exit;
end;
if not TCPClient.Connected then
begin
try
Username.Enabled := false;
Memo1.Lines.Clear;
TCPClient.Host := '127.0.0.1';
TCPClient.Port := 8002;
TCPClient.ConnectTimeout := 5000;
TCPClient.Connect;
Connect.Text := 'Disconnect';
except
on E: Exception do
Memo1.Lines.Add(' ====== ERROR ======' + sLineBreak +
' > ' + E.Message + sLineBreak);
end;
end
else
begin
TCPClient.Disconnect;
Username.Enabled := true;
Connect.Text := 'Connect';
end;
end;
//IF YOU FORGET TO DISCONNECT WHEN APP IS CLOSED
procedure TFormClient.FormDestroy(Sender: TObject);
begin
if TCPClient.Connected then
TCPClient.Disconnect;
end;
//Here I send a string to the server and it's good
procedure TFormClient.SendClick(Sender: TObject);
begin
if TCPClient.Connected then
begin
TCPClient.IOHandler.WriteLn(Username.Text + ': ' + EditMessage.Text);
EditMessage.Text := '';
end
else
begin
Memo1.Lines.Add('ERROR: You aren''t connected!');
end;
end;
//Problems here
procedure TFormClient.Timer1Timer(Sender: TObject);
begin
Memo1.Lines.Add(TCPClient.IOHandler.ReadLn());
end;
The problems start in the last procedure Timer1Timer. I have found that TCPServer uses a thread and this is why I call Synchronize to update the UI. Instead TCPClient does not use a thread and I manually have to check the server. Please see this code:
procedure TFormServer.TCPServerExecute(AContext: TIdContext);
var
txt: string;
begin
txt := AContext.Connection.IOHandler.ReadLn();
AContext.Connection.IOHandler.WriteLn(txt);
TThread.Synchronize(nil, procedure
begin
Memo1.Lines.Add(TimeNow + txt);
end);
end;
As you can see, when the server receives a string he immediatly sends it back to all the clients. I try to get the string here:
procedure TFormClient.Timer1Timer(Sender: TObject);
begin
Memo1.Lines.Add(TCPClient.IOHandler.ReadLn());
end;
What is wrong? I have seen a similar question here and answers said that I have to use a timer and IOHandler.ReadLn(), which is what I am doing. I think that the problem is here. How to fix?
Also timer has an interval of 200, is it too short?
I have read what Remy Lebeau said in the answer and I've produced this simple code:
procedure TFormClient.Timer1Timer(Sender: TObject);
begin
if not(TCPClient.Connected) then
Exit;
if TCPClient.IOHandler.InputBufferIsEmpty then
Exit;
Memo1.Lines.Add(TCPClient.IOHandler.InputBufferAsString());
end;
There is a Timer1 component in the form. This works as I expect but it still could lock the UI or not?
The server works
Just an FYI, you don't need the FClients variable at all, especially since you are not really accessing it safely. At the very least, use TInterlocked to access it safely. Or switch to TIdThreadSafeInteger. Though really, the only place you use it is in LabelCount, and you can get the current client count from the TIdTCPServers.Contexts property instead.
This is the client instead:
...
The problems start in the last procedure Timer1Timer.
That is because you are using a UI-based TTimer, and (like most things in Indy) the IOHandler.ReadLn() method blocks until completed. You are calling it within the context of the UI thread, so it blocks the UI message loop until a full line has arrived from the socket.
One way to work around blocking the UI is to place an Indy TIdAntiFreeze component onto your Form. Then the UI will remain responsive while ReadLn() blocks. However, this would be kind of dangerous to use with a TTimer, as you would end up with OnTimer reentry issues that could corrupt the IOHandler's data.
Really, the best solution is to simply not call the IOHandler.ReadLn() in the UI thread at all. Call it in a worker thread instead. Start the thread after you successfully Connect() to the server, and terminate the thread when disconnected. Or even move the Connect() itself into the thread. Either way, you can use Indy's TIdThreadComponent, or write your own T(Id)Thread-derived class.
Instead TCPClient does not use a thread and I manually have to check the server.
Correct, but the way you are doing it is wrong.
If you don't want to use a worker thread (which you should), then at least change your OnTimer event handler so it does not block the UI anymore, like this:
Or:
procedure TFormClient.Timer1Timer(Sender: TObject);
begin
if TCPClient.IOHandler.InputBufferIsEmpty then
begin
TCPClient.IOHandler.CheckForDataOnSource(0);
TCPClient.IOHandler.CheckForDisconnect(False);
if TCPClient.IOHandler.InputBufferIsEmpty then Exit;
end;
// may still block if the EOL hasn't been received yet...
Memo1.Lines.Add(TCPClient.IOHandler.ReadLn);
end;
Alternatively:
procedure TFormClient.Timer1Timer(Sender: TObject);
begin
// Connected() performs a read operation and will buffer
// any pending bytes that happen to be received...
if not TCPClient.Connected then Exit;
while TCPClient.IOHandler.InputBuffer.IndexOf(Byte($0A)) <> -1 do
Memo1.Lines.Add(TCPClient.IOHandler.ReadLn());
end;
I have seen a similar question here and answers said that I have to use a timer and IOHandler.ReadLn(), which is what I am doing.
Whoever said that is wrong, or you misunderstood what was required. Using a timer in the UI is a possible solution, if used correctly, but it is not a very good solution.
Create a thread for your tcpclient and sync messages back to the UI.
I have a TIdPop3Server in one application that has a IdServerIOHandlerSSLOpenSSL1 attached to it and retrieves emails and sends them to a TIdPop3 client in another application (having TIdSSLIOHandlerSocketOpenSSL attached to it). Everything's fine when the connections are made insecure using port 110. But when I try to use SSL connection through port 995 I get error Connection Closed Gracefully after connect attemp from the client fails. This is my Pop3SeverOnConnect event :
procedure TMainForm.Pop3ServerConnect(AContext: TIdContext);
begin
if (AContext.Connection.IOHandler is TIdSSLIOHandlerSocketBase) then
TIdSSLIOHandlerSocketBase(AContext.Connection.IOHandler).PassThrough :=
(AContext.Binding.Port <> 995);
showmessage('SSL connection made!');
end;
And this is the client-side :
procedure TMainForm.btnCheckMailBoxClick(Sender: TObject);
begin
IdSSLIOHandlerSocketOpenSSL1.PassThrough := False;
POP3Client.IOHandler := IdSSLIOHandlerSocketOpenSSL1;
with POP3Client do begin
AuthType := patUserPass;
Host := myHost;
UserName := myUserName;
Password := myPass;
Port := myPort;
end;
try
POP3Client.Connect;
Except on e : Exception do
showmessage('error=' + e.Message);
end;
// code for retrieving message data
end;
And I always get an exception from Pop3Client.Connect like I've already mentioned above (The message SSL connection made! in the server application never shows up). If I use however another mail client like for example Mozilla Thunderbird I achieve a successful SSL connection for port 995. So the problem should be somewhere in the client's procedure but who knows - that's why I'm asking you guys for help.
In your client code, you need to set the TIdPOP3.UseTLS property instead of the TIdSSLIOHandlerSocketOpenSSL.PassThrough property directly, eg:
procedure TMainForm.btnCheckMailBoxClick(Sender: TObject);
begin
with POP3Client do
begin
IOHandler := IdSSLIOHandlerSocketOpenSSL1;
AuthType := patUserPass;
UseTLS := utUseImplicitTLS; // <-- here
Host := myHost;
UserName := myUserName;
Password := myPass;
Port := myPort;
end;
try
POP3Client.Connect;
try
// code for retrieving message data
finally
POP3Client.Disconnect;
end;
except
on e : Exception do
ShowMessage('error=' + e.Message);
end;
end;
In your server code, you need to get rid of the ShowMessage(). TIdPOP3Server is multi-threaded, the OnConnect event is fired in the context of a worker thread, and ShowMessage() is not thread-safe. If you must display a popup message, use Windows.MessageBox() instead.
I have this TCPServerExecute event that I want to stop from executing after I manually disconnect the connection with the client :
procedure TMainForm.TCPServerExecute(AContext: TIdContext);
var
TCPClient : TIdTCPClient;
begin
try
TCPClient := nil;
try
TCPClient := TIdTCPClient.Create(nil);
if aConditionIsMet then begin
AContext.Connection.IOHandler.WriteLn('Disconnected from server.');
AContext.Connection.Disconnect;
Exit;
end;
finally
FreeAndNil(TCPClient);
end;
except on e : Exception do
begin
MainForm.Log('error in Execute=' + e.Message);
end;
end;
end;
and at client side everything's fine but on server-side I loop through TCPServerExecute infinitely. What am I doing wrong and how can I stop TCPServerExecute from executing after I type AContext.Connection.Disconnect ?
The loop continues because Indy exceptions are not handled correctly.
Either remove the exception handler, or re-raise the exception after logging:
except
on e : Exception do
begin
MainForm.Log('error in Execute=' + e.Message);
raise;
end;
end;
p.s. accessing the MainForm from the server thread is not thread-safe. There are many solutions to do improve this code (TThread.Queue is one of them).
I am working with Indy10 in Delphi XE4, and am trying to make a simple TCP connection with a TCP server on the network. The server sends a simple Hello World message, and then disconnects, and waits for another connection. I have the following code, to connect, and to try and read the message from the server.
with Client do
begin
Host := '10.10.81.122';
Port := 3490;
Connect;
if IOHandler.Connected then
writeln('Connected!');
if IOHandler.InputBufferIsEmpty then
begin
if IOHandler.CheckForDataOnSource(1000) then
begin
writeln('Buffer size:' + IntToStr(IOHandler.RecvBufferSize));
IOhandler.ReadBytes(buffer, IOHandler.RecvBufferSize);
end
else
writeln('Message not received');
end;
end;
When I run this code, I get a value for the recvBufferSize, but the buffer remains empty and then I get a connection terminated gracefully message.
The RecvBufferSize has nothing to do with how many data is actually available for reading. The RecvBufferSize merely specifies the size that Indy uses when allocating internal buffers for reading raw data from the socket. Whatever raw data is actually read gets placed in the IOHandler.InputBuffer for later use by ReadBytes() and other IOHandler reading methods.
Use this code instead:
with Client do
begin
Host := '10.10.81.122';
Port := 3490;
try
Connect;
WriteLn('Connected!');
if IOHandler.CheckForDataOnSource(1000) then
begin
WriteLn(Bytes available:' + IntToStr(IOHandler.InputBuffer.Size));
IOHandler.ReadBytes(buffer, IOHandler.InputBuffer.Size);
end else
WriteLn('Message not received');
except
on E: Exception do
WriteLn('Error! ' + E.Message);
end;
end;
Alternatively:
with Client do
begin
Host := '10.10.81.122';
Port := 3490;
ReadTimeout := 1000;
try
Connect;
WriteLn('Connected!');
IOHandler.ReadBytes(buffer, -1);
if Length(buffer) > 0 then
WriteLn('Bytes read:' + IntToStr(Length(buffer)))
else
WriteLn('Message not received');
except
on E: Exception do
WriteLn('Error! ' + E.Message);
end;
end;
That being said, that is no guaratee that you will actually receive the full message, since this code is merely reading whatever raw data is available at the moment of reading. Since your server is sending a line break at the end of the data, you can use ReadLn() instead of ReadBytes() and let Indy wait until the line break is actually received:
with Client do
begin
Host := '10.10.81.122';
Port := 3490;
ReadTimeout := 1000;
try
Connect;
WriteLn('Connected!');
Msg := IOHandler.ReadLn;
if not IOHandler.ReadLnTimedOut then
WriteLn('Message read:' + Msg)
else
WriteLn('Message not received');
except
on E: Exception do
WriteLn('Error! ' + E.Message);
end;
end;
Alternatively:
with Client do
begin
Host := '10.10.81.122';
Port := 3490;
try
Connect;
WriteLn('Connected!');
Msg := IOHandler.ReadLn(LF, 1000);
if not IOHandler.ReadLnTimedOut then
WriteLn('Message read:' + Msg)
else
WriteLn('Message not received');
except
on E: Exception do
WriteLn('Error! ' + E.Message);
end;
end;