Search code examples
delphiindy

Indy HTTPS POST Request in Delphi using TLS 1.0 instead of TLS 1.2


I'm trying to make an HTTPS Post request in Delphi XE6 and Indy (v10.6.2.0).

I was able to successfully perform the call to this API and get the response using JavaScript:

JavaScript code causing API response to output twice

But I need to do the same in a Delphi application.

I tried multiple attempts using various configurations with TIdSSLIOHandlerSocketOpenSSL, but am getting the error:

Could not load SSL library

The libeay32.dll was the culprit. I also went to the GitHub repo for Indy and wasn't sure which dependent file I needed to download.

The code below is giving me the error:

Cannot open file "C:\Users.. \IndyHTTPSTest\Win32\Debug\dXNlcm5hbWU6cGFzc3dvcmQ=". The system cannot find the file specified.

What am I missing to get this to run in Delphi XE6 like the JavaScript code?

I have included libeay32.dll and ssleay32.dll within my project folder.

Here's my code:

unit uMainForm;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, IdBaseComponent, IdSSLOpenSSL, IdSSLOpenSSLHeaders,
  System.NetEncoding, IdComponent, IdTCPConnection, IdTCPClient, IdHTTP;

type
  TMainForm = class(TForm)
    IdHTTP1: TIdHTTP;
    GetBtn: TButton;
    ResponseMemo: TMemo;
    procedure GetBtnClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;
  ss: TStringStream;
  s, sLog, sFile : String;
  Base64: TBase64Encoding;
  Txt: TextFile;

implementation

{$R *.dfm}

procedure TMainForm.FormCreate(Sender: TObject);
begin
  // Assign JSON to TStringStream
  ss := TStringStream.Create('locations= [{"address":"Test1","lat":"52.05429","lng":"4.248618"},{"address":"Test2","lat":"52.076892","lng":"4.26975"},{"address":"Test3","lat":"51.669946","lng":"5.61852"},{"address":"Sint-Oedenrode, The Netherlands","lat":"51.589548","lng":"5.432482"}]', TEncoding.UTF8);
  // Base64 encode username and password string to satisfy API for "Authorization", "Basic " + sLog);
  s := 'username:password';
  Base64 := TBase64Encoding.Create(20, '');
  sLog := Base64.Encode(s);
end;

procedure TMainForm.GetBtnClick(Sender: TObject);
var
  responseFromServer, sWhichFail, sVer :string;
  LHandler: TIdSSLIOHandlerSocketOpenSSL;
begin
  LHandler := TIdSSLIOHandlerSocketOpenSSL.Create(self);
  with LHandler do
    begin
      SSLOptions.Method := sslvSSLv2;
      SSLOptions.Mode := sslmUnassigned;
      SSLOptions.VerifyMode := [];
      SSLOptions.VerifyDepth := 0;
      host := '';
    end;

  IdHTTP1 := TIdHTTP.Create(Self);
  with IdHTTP1 do
    begin
      IOHandler := LHandler;
      AllowCookies := True;
      ProxyParams.BasicAuthentication := False;
      ProxyParams.ProxyPort := 0;
      Request.ContentLength := -1;
      Request.ContentType := 'application/x-www-form-urlencoded';
      Request.ContentRangeEnd := 0;
      Request.ContentRangeStart := 0;
      Request.Accept := 'text/json, */*';
      Request.BasicAuthentication := True;
      Request.UserAgent := 'Mozilla/3.0 (compatible; Indy Library)';

      HTTPOptions := [hoForceEncodeParams];
    end;

    try
      // Set up the request and send it
        sFile:= IdHTTP1.Post('https://api.routexl.com/tour', sLog);
        ResponseMemo.Lines.Add(Format('Response Code: %d', [IdHTTP1.ResponseCode]));
        ResponseMemo.Lines.Add(Format('Response Text: %s', [IdHTTP1.ResponseText]));
    finally
      sWhichFail:= WhichFailedToLoad();
      ShowMessage(sWhichFail);
      ResponseMemo.Lines.Text:= sWhichFail;
      //sVer := OpenSSLVersion();
      //ShowMessage(sVer);
    end;
end;

end.

UPDATE: I have updated the code, and added screenshots to help with debugging:

unit uMainForm;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, IdBaseComponent, IdSSLOpenSSL, IdSSLOpenSSLHeaders,
  System.NetEncoding, IdComponent, IdTCPConnection, IdTCPClient, IdHTTP;

type
  TMainForm = class(TForm)
    IdHTTP1: TIdHTTP;
    GetBtn: TButton;
    ResponseMemo: TMemo;
    procedure GetBtnClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private declarations }
    sl: TStringList;
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.FormCreate(Sender: TObject);
begin
  sl := TStringList.Create;
  sl.Add('locations=[{"address":"Test1","lat":"52.05429","lng":"4.248618"},{"address":"Test2","lat":"52.076892","lng":"4.26975"},{"address":"Test3","lat":"51.669946","lng":"5.61852"},{"address":"Sint-Oedenrode, The Netherlands","lat":"51.589548","lng":"5.432482"}]');
end;

procedure TMainForm.FormDestroy(Sender: TObject);
begin
  sl.Free;
end;

procedure TMainForm.GetBtnClick(Sender: TObject);
var
  LHTTP: TIdHTTP;
  LHandler: TIdSSLIOHandlerSocketOpenSSL;
  responseFromServer, sWhichFail, sVer :string;
begin
  ResponseMemo.Clear;

  LHTTP := TIdHTTP.Create(nil);
  try
    LHandler := TIdSSLIOHandlerSocketOpenSSL.Create(LHTTP);
    LHandler.SSLOptions.SSLVersions := [sslvTLSv1_2];
    LHandler.SSLOptions.Mode := sslmClient;
    LHandler.SSLOptions.VerifyMode := [];
    LHandler.SSLOptions.VerifyDepth := 0;

    LHTTP.IOHandler := LHandler;
    LHTTP.AllowCookies := True;
    LHTTP.Request.Accept := 'text/json, */*';
    LHTTP.Request.BasicAuthentication := True;
    LHTTP.Request.ContentType := 'application/x-www-form-urlencoded';
    LHTTP.Request.Username := 'username:';
    LHTTP.Request.Password := 'password';

    LHTTP.HTTPOptions := [hoForceEncodeParams];

    try
      responseFromServer := IdHTTP1.Post('https://api.routexl.com/tour', sl);
      ResponseMemo.Lines.Text := responseFromServer;
    except
      on E: EIdOSSLCouldNotLoadSSLLibrary do
      begin
        sVer := OpenSSLVersion();
        sWhichFail := WhichFailedToLoad();
        ResponseMemo.Lines.Add(sVer);
        ResponseMemo.Lines.Add(sWhichFail);
        ShowMessage('Could not load OpenSSL');
      end;
      on E: EIdHTTPProtocolException do
      begin
        ResponseMemo.Lines.Add(Format('Response Code: %d', [LHTTP.ResponseCode]));
        ResponseMemo.Lines.Add(Format('Response Text: %s', [LHTTP.ResponseText]));
        ResponseMemo.Lines.Add(E.ErrorMessage);
        ShowMessage('HTTP Failure');
      end;
      on E: Exception do
      begin
        ResponseMemo.Lines.Add(E.ClassName);
        ResponseMemo.Lines.Add(E.Message);
        ShowMessage('Unknown Error');
      end;
    end;
  finally
    LHTTP.Free;
  end;
end;

end. 

WireShark is showing the following when executing the JavaScript code:

image

WireShark is showing the following when executing the Delphi code:

image


Solution

  • When you get an OpenSSL load error from Indy, you can call Indy's WhichFailedToLoad() function in the IdSSLOpenSSLHeaders unit to find out why. Which you are, but you didn't show us what it is actually saying, so we can't diagnose why OpenSSL is failing to load. Typically, the load will fail because either:

    • libeay32.dll and/or ssleay32.dll cannot be found. If they are not in the same folder as your program, or at least on the OS' DLL search path, then you can use Indy's IdOpenSSLSetLibPath() function in the IdSSLOpenSSLHeaders unit to tell Indy where to look for them.

    • they are a different bitness than your program. A 32bit program cannot load 64bit DLLs, and vice versa.

    • they are missing required exported functions, which would indicate that they are likely a version of OpenSSL that Indy simply does not support. For instance, TIdSSLIOHandlerSocketOpenSSL supports only up to OpenSSL 1.0.2u. For OpenSSL 1.1.x+, you need to use this experimental SSLIOHandler instead.

    With that said, I see several things wrong with your Post code:

    • you are manually encoding input data that you don't need to encode manually at all. TIdHTTP.Post() can handle that internally for you.

    • you are calling the version of TIdHTTP.Post() that takes a String filename as input, which will open the specified file and send its content as-is. But you are not passing in a filename, you are passing in your Base64-encoded user credentials (not even your JSON). To make a proper application/x-www-webform-urlencoded post, you need to use the version of TIdHTTP.Post() that takes a TStrings of name=value pairs as input.

    • TIdHTTP natively implements Basic authentication, so you should be using the TIdHTTP.Request.UserName and TIdHTTP.Request.Password properties for your user credentials.

    • you are telling TIdSSLIOHandlerSocketOpenSSL to use SSL v2.0. Nobody uses that anymore, as it is no longer secure. You should not be using anything less than TLS v1.0 as an absolute minimum. And even that is being phased out on most servers. So make sure you also have TLS v1.1 and TLS v1.1 enabled, too (for TLS v1.3, you need to use the other SSLIOHandler).

    Frankly, your Delphi code is not even close to replicating what the Javascript code is doing. Try something more like this instead:

    unit uMainForm;
    
    interface
    
    uses
      Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
      Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, IdBaseComponent, IdSSLOpenSSL, IdSSLOpenSSLHeaders,
      System.NetEncoding, IdComponent, IdTCPConnection, IdTCPClient, IdHTTP;
    
    type
      TMainForm = class(TForm)
        GetBtn: TButton;
        ResponseMemo: TMemo;
        procedure GetBtnClick(Sender: TObject);
        procedure FormCreate(Sender: TObject);
        procedure FormDestroy(Sender: TObject);
      private
        { Private declarations }
        sl: TStringList;
      public
        { Public declarations }
      end;
    
    var
      MainForm: TMainForm;
    
    implementation
    
    {$R *.dfm}
    
    procedure TMainForm.FormCreate(Sender: TObject);
    begin
      sl := TStringList.Create;
      sl.Add('locations=[{"address":"Test1","lat":"52.05429","lng":"4.248618"},{"address":"Test2","lat":"52.076892","lng":"4.26975"},{"address":"Test3","lat":"51.669946","lng":"5.61852"},{"address":"Sint-Oedenrode, The Netherlands","lat":"51.589548","lng":"5.432482"}]');
    end;
    
    procedure TMainForm.FormDestroy(Sender: TObject);
    begin
      sl.Free;
    end;
    
    procedure TMainForm.GetBtnClick(Sender: TObject);
    var
      LHTTP: TIdHTTP;
      LHandler: TIdSSLIOHandlerSocketOpenSSL;
      responseFromServer, sWhichFail, sVer :string;
    begin
      ResponseMemo.Clear;
    
      LHTTP := TIdHTTP.Create(nil);
      try
        LHandler := TIdSSLIOHandlerSocketOpenSSL.Create(LHTTP);
        LHandler.SSLOptions.SSLVersions := [sslvTLSv1, sslvTLSv1_1, sslvTLSv1_2];
        LHandler.SSLOptions.Mode := sslmClient;
        LHandler.SSLOptions.VerifyMode := [];
        LHandler.SSLOptions.VerifyDepth := 0;
    
        LHTTP.IOHandler := LHandler;
        LHTTP.AllowCookies := True;
        LHTTP.Request.Accept := 'text/json, */*';
        LHTTP.Request.BasicAuthentication := True;
        LHTTP.Request.Username := 'username';
        LHTTP.Request.Password := 'password';
    
        LHTTP.HTTPOptions := [hoForceEncodeParams];
    
        try
          responseFromServer := IdHTTP1.Post('https://api.routexl.com/tour', sl);
          ResponseMemo.Lines.Text := responseFromServer;
        except
          on E: EIdOSSLCouldNotLoadSSLLibrary do
          begin
            sVer := OpenSSLVersion();
            sWhichFail := WhichFailedToLoad();
            ResponseMemo.Lines.Add(sVer);
            ResponseMemo.Lines.Add(sWhichFail);
            ShowMessage('Could not load OpenSSL');
          end;
          on E: EIdHTTPProtocolException do
          begin
            ResponseMemo.Lines.Add(Format('Response Code: %d', [LHTTP.ResponseCode]));
            ResponseMemo.Lines.Add(Format('Response Text: %s', [LHTTP.ResponseText]));
            ResponseMemo.Lines.Add(E.ErrorMessage);
            ShowMessage('HTTP Failure');
          end;
          on E: Exception do
          begin
            ResponseMemo.Lines.Add(E.ClassName);
            ResponseMemo.Lines.Add(E.Message);
            ShowMessage('Unknown Error');
          end;
        end;
      finally
        LHTTP.Free;
      end;
    end;
    
    end.
    

    UPDATE: that being said, the API documentation shows the following curl example:

    Get an unoptimized route using cURL:

    curl \
    --url https://api.routexl.com/tour/ \
    --user <username>:<password> \
    --data 'locations=[{"address":"1","lat":"52.05429","lng":"4.248618"},{"address":"2","lat":"52.076892","lng":"4.26975"},{"address":"3","lat":"51.669946","lng":"5.61852"},{"address":"4","lat":"51.589548","lng":"5.432482"}]&skipOptimisation=true'
    

    That example DOES translate to the above Delphi code, per the curl documentation:

    -d, --data

    (HTTP MQTT) Sends the specified data in a POST request to the HTTP server, in the same way that a browser does when a user has filled in an HTML form and presses the submit button. This will cause curl to pass the data to the server using the content-type application/x-www-form-urlencoded. Compare to -F, --form.

    With the exception of one detail - when sending an application/x-www-form-urlencoded request with the hoForceEncodeParams option enabled, TIdHTTP.Post() will percent-encode the [], {}, ", :, and , characters in the JSON in %HH format. But the API's Javascript and PHP examples on GitHub are not doing that encoding, they send the JSON as-is. So, if it turns out the API server does not like that encoding, then simply disable the hoForceEncodeParams option:

    LHTTP.HTTPOptions := [];