TIdHTTP 通过代理将 HTTPS 请求作为 HTTP 发送

TIdHTTP sends HTTPS request as HTTP over proxy

我在使用 TIdHTTP 实施 SSL 证书固定时遇到问题。

所以,步骤如下:

  1. 在表单上放置 TIdHTTP、TIdSSLIOHandlerSocketOpenSSL 和 TIdCompressorZLib。
  2. 将 TIdSSLIOHandlerSocketOpenSSL 和 TIdCompressorZLib 分配给 TIdHTTP 的 IOHandler 和 Compressor 属性。
  3. 设置 TIdSSLIOHandlerSocketOpenSSL:

    Port = 0
    DefaultPort = 0
    SSLOptions.Method = sslvTLSv1_2
    SSLOptions.SSLVersions = [sslvTLSv1_2]
    SSLOptions.Mode = sslmClient
    SSLOptions.VerifyMode = [sslvrfPeer]
    SSLOptions.VerifyDepth = 0
    OnVerifyPeer = SSLIOHandlerVerifyPeer
    
  4. SSLIOHandlerVerifyPeer 代码:

    function TForm2.SSLIOHandlerVerifyPeer(Certificate: TIdX509; AOk: Boolean; ADepth, AError: Integer): Boolean;
    const
      LCGoogleCert = '98:1D:34:C4:F8:4A:F2:B7:C7:AB:77:AD:51:1C:51:4C:AD:76:ED:0D:0E:FA:C9:63:68:AF:28:69:94:60:BF:7A';
    begin
      Result := Certificate.Fingerprints.SHA256AsString.Equals(LCGoogleCert);
    end;
    
  5. 在表单上放置一个按钮:

    procedure TForm2.Button1Click(Sender: TObject);
    const
      LCGoogleURL = 'https://www.google.com/';
    var
      s: UnicodeString;
    begin
      s := HTTPSender.Get(LCGoogleURL);
    end;
    
  6. 安装Fiddler

  7. 在 Fiddler 中:工具 - 选项 - 选中 "Capture HTTPS Connects" 并取消选中 "Decrypt HTTPS traffic"。生成证书并将其安装到系统。

  8. 将Fiddler的代理地址和端口设置为TIdHTTP

  9. 运行 程序并单击按钮。第一次单击 - 您会得到关于证书不正确的异常。但是如果你第二次点击 - 你不会得到任何异常,但你会得到完整的响应,你会在 Fiddler 中看到未加密的流量,就像你通过 HTTP 而不是 HTTPS 发送请求一样。

您可以在下图中看到第一次和第二次请求的结果。这是 Indy 组件中的错误,还是我试图错误地实施 SSL Pinning?

在第一次调用 TIdHTTP.Get() 时,OnVerifyPeer 事件看到了 Fiddler 的 SSL/TLS 证书而不是 Google 的证书,因此它拒绝了证书,并且底层套接字连接最终被关闭。

但是,在 IOHandler 的 InputBuffer 中遗留了未读数据(大约 162 字节的加密数据)。 根据设计TIdIOHandlerStack.Connected()方法returns只要有未读数据可用于满足读取操作,即使没有物理套接字连接也是如此。

因此,在第二次调用 TIdHTTP.Get() 期间最终发生的情况如下:

  • TIdHTTP 知道它正在通过代理通过 HTTPS 进行通信,并且它正在通过与先前相同的代理向同一 Google 服务器发出新的 HTTP 请求致电 TIdHTTP.Get()。所以 TIdHTTP 检查 Connected() 看它是否仍然连接到代理,并且看到 Connected() 最初是 True,所以它决定跳过一个新的 CONNECT 请求并像通过现有代理连接发送新的 HTTP 请求一样继续。

  • 但是,由于底层套接字已断开连接,TIdHTTP 必须与 Fiddler 建立新的套接字连接。在准备新的 HTTP 请求时,InputBuffer 被清除。 Connected() 再次检查,现在为 False,因此 TIdHTTP 与 Fiddler 建立新的套接字连接。新的套接字连接最初是未加密的(IOHandler 的 PassThrough 设置为 True)因此后续的 CONNECT 不会被加密(但这部分代码不知道 TIdHTTP 已经决定跳过 CONNECT).

  • TIdHTTP 继续向 Fiddler 发送未加密的 GET 请求。

  • Fiddler 将其 TLS 隧道缓存一段时间,因此它将现有隧道重用到 Google,从而将未加密的 GET 按原样转发到 Google 通过 TLS 连接,然后将未加密的响应转发回 TIdHTTP.

所以,归根结底,这里有三个问题在起作用(我已经打开了 ticket in Indy's issue tracker):

    当发生需要关闭底层套接字的故障时,
  • TIdHTTP 不会清除 InputBuffer。一个简单的解决方法是让 TIdCustomHTTP.ConnectToHost() 方法在执行任何其他操作之前清除任何现有数据的 InputBuffer。这样,它会在决定如何处理 CONNECT 之前看到连接真的消失了。我现在已经将此修复程序签入 Indy 的 SVN 存储库,并测试它在您的场景中是否有效。

  • TIdHTTP 过早地决定发送或跳过 CONNECT,在它知道它正在对底层套接字做什么之前。这将需要对 TIdHTTP 的内部逻辑进行一些重写,因此将推迟到更高版本的 Indy。

  • Fiddler 正在通过先前加密的隧道来回转发未加密的数据。 TIdHTTP.

  • 对此无能为力