Mikey
Mikey

Reputation: 1

Can't connect to Indy TIdHttpServer with TLS

I'm writing a test Delphi 10.4 Win32 application that uses TIdHTTPServer and TIdServerIOHandlerSSLOpenSSL from Indy 10.6.2.0 installed by Delphi 10.4, using a port on localhost for the purposes of communicating between a C# (.NET 5) client application and this Win32 Delphi VCL (server) application.

I know very little about TLS, SSL, and certificates but have been learning as I attempt to build this; from the examples I have seen, I had expected this to be simple.

The "general" page for the .pfx certificate properties says "Enable all purposes for this certificate".

I started partly by reviewing examples and also Remy Lebeau's postings here:

https://stackoverflow.com/a/34422109/17371832

https://stackoverflow.com/a/63082235/17371832

Using Indy 10 IdHTTP with TLS 1.2

I have provided it with OpenSSL 1.0.2.17 DLLs:

libeay32.dll
ssleay32.dll

These DLLs load successfully.

The problem is that clients can't connect to it. .NET says "The SSL connection could not be established" (more about the error inside Indy later).

WireShark shows "Client Hello" (TLS 1.2 with about 20 cipher types) but no "Server Hello", alerts, or errors are sent. In fact, only one packet marked as TLS 1.2 is sent (when the client said "Client Hello").

TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:(and many others)

I had tried providing the full list (as formatted above) to the TIdHTTPServer's CipherList property, but got an error (I think it was "SetCipherList failed").

If the cipher list is left empty, it seems to internally use "AES:ALL:!aNULL:!eNULL:+RC4:@STRENGTH". I left it that way.

Here are the components involved:

object srv: TIdHTTPServer
  Bindings = <
    item
      IP = '127.0.0.1'
      Port = 5500
    end>
  DefaultPort = 5500
  IOHandler = iohSSL
  MaxConnections = 25
  AutoStartSession = True
  KeepAlive = True
  SessionState = True
  OnCreatePostStream = srvCreatePostStream
  OnDoneWithPostStream = srvDoneWithPostStream
  OnCommandGet = srvCommandGet
  Left = 183
  Top = 118
end
object iohSSL: TIdServerIOHandlerSSLOpenSSL
  SSLOptions.CertFile = 'C:\Users\Mike\AppData\Local\Somewhere\cert.pfx'
  SSLOptions.KeyFile = 'C:\Users\Mike\AppData\Local\Somewhere\cert.pfx'
  SSLOptions.Method = sslvTLSv1_2
  SSLOptions.SSLVersions = [sslvTLSv1_2]
  SSLOptions.Mode = sslmServer
  OnGetPasswordEx = iohSSLGetPasswordEx
  Left = 256
  Top = 120
end

The code isn't interesting:

procedure TdmCloud.DataModuleCreate(Sender: TObject);
begin
  iohSSL.SSLOptions.CertFile:='.\cert.pfx';
  //iohSSL.SSLOptions.RootCertFile:='.\root2.pem';
  //iohSSL.SSLOptions.KeyFile:='.\key.pem';

  srv.Active:=true;
end;

procedure TdmCloud.iohSSLGetPasswordEx(ASender: TObject; var VPassword: string;
  const AIsWrite: Boolean);
begin
  VPassword := 'password';
end;

procedure TdmCloud.srvCommandGet(AContext: TIdContext;
  ARequestInfo: TIdHTTPRequestInfo;
  AResponseInfo: TIdHTTPResponseInfo);
begin
  AResponseInfo.ResponseNo := HTTP_OK;
  AResponseInfo.ContentType := 'text/html';
  AResponseInfo.ContentText := '<html><head><title>Title</title></head><body>HELLO</body></html>';
end;

procedure TdmCloud.srvCreatePostStream(AContext: TIdContext;
  AHeaders: TIdHeaderList; var VPostStream: TStream);
begin
  VPostStream := TMemoryStream.Create;
end;

procedure TdmCloud.srvDoneWithPostStream(AContext: TIdContext;
  ARequestInfo: TIdHTTPRequestInfo; var VCanFree: Boolean);
begin
  VCanFree := true;
end;

I have seen in the debugger that the certificate file is loaded successfully with the password provided by the event handler. The listener thread runs ok until it receives a request.

TIdHTTPServer's OnCommandGet event doesn't fire.

I've seen in the debugger that InternalReadLn() returns a string that looks encrypted; it's definitely binary. Anyway, below, I get "error in parsing command"; judging from the data it's looking at, I'm not surprised.

var
  i: integer;
  s, LInputLine, LRawHTTPCommand, LCmd, LContentType, LAuthType: String;
  LURI: TIdURI;
  LContinueProcessing, LCloseConnection: Boolean;
  LConn: TIdTCPConnection;
  LEncoding: IIdTextEncoding;
begin
  LContinueProcessing := True;
  Result := False;
  LCloseConnection := not KeepAlive;
  try
    try
      LConn := AContext.Connection;
      repeat
        // InternalReadLn( ) returns a string that looks encrypted; it's definitely binary
        LInputLine := InternalReadLn(LConn.IOHandler);
        i := RPos(' ', LInputLine, -1);    {Do not Localize}
        if i = 0 then begin   // true
          raise EIdHTTPErrorParsingCommand.Create(RSHTTPErrorParsingCommand);
        end;

As it happens, InternalReadLn() calls TIdServerIOHandlerSSLOpenSSL's Readln() method, which has provided this binary data.

Remy Lebeau said this about the error in an online posting:

The error is actually coming from several lines further down:

i := RPos(' ', LInputLine, -1); {Do not Localize}
if i = 0 then begin
  raise EIdHTTPErrorParsingCommand.Create(RSHTTPErrorParsingCommand);
end;

Which means your client is sending a malformed HTTP request. The first line in EVERY request must end with an HTTP version number, ie: GET /login.html HTTP/1.0 Hence the call to RPos(). The error means there is no space character anywhere in the line.

Although it could certainly be a malformed request, I'm told that the application's requests have been tested and found to be valid/working.

I've tried a browser directed to https://localhost:5500/ with similar results.

That brings me back to the conclusion that TLS negotiation didn't start at "Client hello". I have no idea why. I don't know where that process is handled in Indy.

Here's a typical, working TLS negotiation: https://tls.ulfheim.net/

Key areas of suspicion for me; I've made some effort to explore each possibility:

Unhelpfully, all of documentation links at https://www.indyproject.org/documentation/ are broken! My Delphi install didn't include any Indy help either.

Help being absent, I'm uncertain which to provide of RootCertFile, CertFile, and KeyFile. I've tried the .pfx which was accepted for CertFile and KeyFile but later saw https://stackoverflow.com/a/15416234/17371832 so I followed the instructions and was able to make a .pem file (root file was not accepted by Indy). This provided no improvement.

Is Indy dying? Is there something else that's better? Haven't looked into that at all.

Why won't it connect?

Upvotes: 0

Views: 1642

Answers (1)

Remy Lebeau
Remy Lebeau

Reputation: 597941

The standard TCP port for HTTPS is 443, but you are running your TIdHTTPServer on port 5500 instead. As such, you must use the TIdHTTPServer.OnQuerySSLPort event to explicitly tell TIdHTTPServer that you want to use SSL/TLS on that port, eg:

procedure TdmCloud.srvQuerySSLPort(APort: TIdPort; var VUseSSL: Boolean);
begin
  VUseSSL := APort = 5500;
end;

This event allows HTTP and HTTPS to co-exist on a single server (via multiple TIdHTTPServer.Bindings items) even when using non-standard ports. Without this event handler assigned, this is why your server is not handling the client's SSL/TLS handshake correctly.

Upvotes: 1

Related Questions