Delphi中使用TLS 1.0而不是TLS 1.2进行Indy HTTPS POST请求

3

我正在尝试在Delphi XE6和Indy(v10.6.2.0)中进行HTTPS Post请求。

我已成功使用JavaScript调用此API并获取响应:

Javascript代码,导致API响应输出两次

但我需要在Delphi应用程序中进行相同的操作。

我尝试了多次使用TIdSSLIOHandlerSocketOpenSSL的不同配置,但出现错误:

无法加载SSL库

libeay32.dll是罪魁祸首。我也去了Indy的GitHub仓库,但不确定需要下载哪个依赖文件。

下面的代码给我带来了错误:

无法打开文件“C:\Users..\IndyHTTPSTest\Win32\Debug\dXNlcm5hbWU6cGFzc3dvcmQ =”。系统找不到指定的文件。

我需要怎样才能像JavaScript代码一样在Delphi XE6中运行它?

我已经在我的项目文件夹中包含了libeay32.dllssleay32.dll
以下是我的代码:
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.
更新:我已经更新了代码,并添加了屏幕截图来帮助调试:
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. 

执行JavaScript代码时,WireShark显示以下内容:

image

当执行 Delphi 代码时,WireShark 显示以下内容:

image


1
显然,Post() 的第二个参数是文件名。 - Olivier
2个回答

3
当您从Indy获得OpenSSL加载错误时,您可以调用IdSSLOpenSSLHeaders单元中的Indy的WhichFailedToLoad()函数以找出原因。但是,您并没有向我们展示它实际上在说什么,因此我们无法诊断为什么OpenSSL无法加载。通常,加载失败是因为以下原因之一:
  • libeay32.dll和/或ssleay32.dll无法找到。如果它们不在您的程序所在的文件夹中,或者至少在操作系统的DLL搜索路径上,那么您可以使用Indy的IdSSLOpenSSLHeaders单元中的IdOpenSSLSetLibPath()函数告诉Indy在哪里查找它们。

  • 它们与您的程序的位数不同。32位程序无法加载64位DLL,反之亦然。

  • 它们缺少所需的导出函数,这表明它们很可能是Indy不支持的OpenSSL版本。例如,TIdSSLIOHandlerSocketOpenSSL仅支持最多OpenSSL 1.0.2u。对于OpenSSL 1.1.x+,您需要使用这个实验性的SSLIOHandler

话虽如此,我发现您的Post代码有几个问题:

  • 您正在手动编码输入数据,而实际上您完全不需要手动编码。 TIdHTTP.Post() 可以在内部为您处理。

  • 您正在调用以 String 文件名作为输入的 TIdHTTP.Post() 版本,它将打开指定的文件并按原样发送其内容。但是您没有传递文件名,而是传递了您的 Base64 编码的用户凭据(甚至不是您的 JSON)。要进行正确的 application/x-www-webform-urlencoded 发布,您需要使用将 TStringsname=value 对作为输入的 TIdHTTP.Post() 版本。

  • TIdHTTP 本地实现了基本身份验证,因此您应该使用 TIdHTTP.Request.UserNameTIdHTTP.Request.Password 属性来进行用户凭证。

  • 您正在告诉 TIdSSLIOHandlerSocketOpenSSL 使用 SSL v2.0。现在没有人再使用它,因为它已不再安全。您不应该使用低于 TLS v1.0 的任何内容作为绝对最小值。即使在大多数服务器上,这也正在被淘汰。因此,请确保您还启用了 TLS v1.1 和 TLS v1.1(对于 TLS v1.3,您需要使用 其他 SSLIOHandler)。

坦白地说,您的Delphi代码甚至无法复制JavaScript代码正在执行的操作。请尝试使用类似以下代码的内容:
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.
更新: 话虽如此,API文档 显示以下curl示例:

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'

根据curl文档,该示例确实可以转换为上面的Delphi代码:

-d, --data

(HTTP MQTT)将指定数据发送到HTTP服务器的POST请求中,就像用户填写HTML表单并按提交按钮一样。这将导致curl使用content-type application/x-www-form-urlencoded将数据传递给服务器。与-F, --form相比较。

除了一个细节 - 当启用hoForceEncodeParams选项发送application/x-www-form-urlencoded请求时,TIdHTTP.Post()将会对[], {}, ", :, 和 ,字符在JSON中进行百分号编码%HH。但是GitHub上API的Javascript和PHP示例没有进行这种编码,它们直接发送JSON。所以,如果API服务器不喜欢这种编码方式,那么只需禁用hoForceEncodeParams选项即可:

LHTTP.HTTPOptions := [];

我正在使用你的代码。现在我遇到了错误“EIdSocketError Socket Error # 10054 Connection reset by peer。”。我分析了我的工作JavaScript代码,看看是否能确定是什么原因导致了这个问题。从我的JS程序中删除此行httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');会导致此错误“No input found”。此外,我的JavaScript需要在处理服务器响应之前对ReadyState进行检查。我唯一看到的其他潜在问题是用户名:密码。将其作为两行而不带分号是否可以? - Hackbrew
基本身份验证最容易通过使用 TIdHTTP.Request.UserName/PasswordTIdHTTP.Request.BasicAuthentication=True 来处理。至于 API,我已经更新了我的答案。 - Remy Lebeau
服务器正在强制断开您的客户端连接。由于请求是通过HTTPS进行的,因此服务器可能不喜欢您的客户端TLS握手,在HTTP请求甚至传输之前。我建议使用数据包嗅探器(如Wireshark)来比较Javascript的TLS握手和TIdHTTP的TLS握手,以查看它们的区别。如果我猜测的话,由于您正在使用Delphi XE6,您可能还在使用一个不支持TLS的SNI扩展的旧版Indy。现在大多数TLS 1.2服务器都需要SNI。Indy已经支持客户端SNI 5年了,比XE6发布时间晚2年。 - Remy Lebeau
OpenSSL 1.0.2.14(1.0.2n)有点旧了,1.0.2系列的最后一个版本是1.0.2.21(1.0.2u)。您可以从GitHub上Indy的OpenSSL-Binaries repo下载更新的DLL。请不要仅仅描述问题,编辑您的问题以添加相关细节、截图等。您是否在Javascript的TLS握手中看到SNI主机名条目?在Indy的握手中呢? - Remy Lebeau
@Hackbrew,你更新了DLL文件,但是你是否也像我之前建议的那样更新了Indy本身呢?XE6附带的版本已经过时,还不支持TLS1.2(客户端SNI、默认密码列表等)。我猜后者特别是你的问题所在。如果我没记错的话,Indy以前只启用TLS1.0密码。您可以更新CipherList属性以启用TLS 1.2密码。但是缺少SNI支持将成为一个问题。 - Remy Lebeau
显示剩余21条评论

0

你可以将 sLog: String 的声明更改为 sLog: TStrings。然后在 onCreate 中实例化 sLog := TStringList.Create,然后稍后在代码中使用 sLog.Text := Base64.Encode(s);

所以,

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

procedure TMainForm.FormCreate(Sender: TObject);
begin
  sLog := TStringList.Create;
  // 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.Text := Base64.Encode(s);
end;

这个回答完全没有解决任何OP的问题。不是为什么OpenSSL无法加载,也不是为什么JSON无法发送到服务器,也不是为什么用户身份验证会失败。你所做的就是修复了TIdHTTP.Post()输入,使用TStringList而不是String - 这是发送application/x-www-webform-urlencoded请求的正确方法,但是TStringList仍然使用完全错误的数据(用户凭据而不是JSON)。 - Remy Lebeau
@RemyLebeau,非常抱歉,我专注于帖子中的“下面的代码给了我错误:”这一行,所以我试图为此提供解释。下次我会更努力的。 - Mark

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接