【问题标题】:Indy HTTPS POST Request in Delphi using TLS 1.0 instead of TLS 1.2Delphi 中的 Indy HTTPS POST 请求使用 TLS 1.0 而不是 TLS 1.2
【发布时间】:2021-05-03 17:48:46
【问题描述】:

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

我能够成功调用此 API 并使用 JavaScript 获得响应:

JavaScript code causing API response to output twice

但我需要在 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. 

WireShark 在执行 JavaScript 代码时显示如下:

WireShark 在执行 Delphi 代码时显示如下:

【问题讨论】:

  • 显然Post()的第二个参数是一个文件名。

标签: delphi indy


【解决方案1】:

当您从 Indy 收到 OpenSSL 加载错误时,您可以在 IdSSLOpenSSLHeaders 单元中调用 Indy 的 WhichFailedToLoad() 函数以找出原因。你是谁,但你没有向我们展示它实际上在说什么,所以我们无法诊断为什么 OpenSSL 无法加载。通常,加载会失败,因为:

  • libeay32.dll 和/或ssleay32.dll 找不到。如果它们与您的程序不在同一个文件夹中,或者至少不在操作系统的 DLL 搜索路径中,那么您可以在 IdSSLOpenSSLHeaders 单元中使用 Indy 的 IdOpenSSLSetLibPath() 函数来告诉 Indy 在哪里寻找它们。

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

  • 它们缺少必需的导出函数,这表明它们可能是 Indy 根本不支持的 OpenSSL 版本。例如,TIdSSLIOHandlerSocketOpenSSL 最多仅支持 OpenSSL 1.0.2u。对于 OpenSSL 1.1.x+,您需要改用 this experimental SSLIOHandler

话虽如此,我发现您的邮政编码有几个问题:

  • 您正在手动编码根本不需要手动编码的输入数据。 TIdHTTP.Post() 可以在内部为您处理。

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

  • TIdHTTP 原生实现Basic 身份验证,因此您应该使用TIdHTTP.Request.UserNameTIdHTTP.Request.Password 属性作为您的用户凭据。

  • 您告诉TIdSSLIOHandlerSocketOpenSSL 使用 SSL v2.0。没有人再使用它了,因为它不再安全。您不应该使用低于 TLS v1.0 的任何东西作为绝对最小值。即使是在大多数服务器上也正在逐步淘汰。因此,请确保您也启用了 TLS v1.1 和 TLS v1.1(对于 TLS v1.3,您需要使用 other 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 documentation 显示以下 curl 示例:

使用 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 documentation,该示例确实转换为上述 Delphi 代码:

-d,--数据

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

除了一个细节 - 当发送一个启用了hoForceEncodeParams 选项的application/x-www-form-urlencoded 请求时,TIdHTTP.Post() 将对[]{}":,%HH 格式的 JSON 中的字符。但是 GitHub 上 API 的 Javascript and PHP examples 没有进行这种编码,它们按原样发送 JSON。因此,如果 API 服务器不喜欢这种编码,那么只需禁用 hoForceEncodeParams 选项:

LHTTP.HTTPOptions := [];

【讨论】:

  • 我正在使用您的代码。现在我收到错误“EIdSocketError Socket Error # 10054 Connection reset by peer.”。我分析了我的工作 JavaScript 代码,看看我是否可以确定是什么导致了这种情况。从我的 JS 程序中删除此行 httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 会导致此错误“未找到输入”。此外,我的 JavaScript 确实需要在处理服务器响应之前检查 ReadyState。我看到的唯一其他潜在问题是用户名:密码。可以将其作为没有分号的两条单独的行吗?
  • 如果我在 POST 之前检查 OpenSSLVersion();,我会在 2017 年 12 月 7 日获得 OpenSSL 1.0.2n。
  • 要消除请求中的 JSON,您可以添加 responseFromServer := IdHTTP1.Get('https://api.routexl.com/v2/status/Hello+world'); 来代替 POST。
  • 但是通过这样做,您不再复制 Javascript 的行为。服务器期望你发送给它的究竟是什么?
  • API 需要使用基于 HTTPS 的基本身份验证的用户名和密码。你是对的,我只是认为这可能会简化事情,但我会继续使用 POST 示例。 Here's a link to API doc
【解决方案2】:

您可以将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)。
  • @RemyLebeau,很抱歉,我专注于帖子中的The code below is giving me the error: 行,所以我试图对此做出解释。下次我会努力的。
猜你喜欢
  • 2022-01-01
  • 1970-01-01
  • 2018-08-06
  • 1970-01-01
  • 1970-01-01
  • 2018-12-09
  • 2018-10-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多