启用TLS的Indy TIdHTTPServer无法被客户端连接问题排查求助
我正在编写一个Delphi 10.4 Win32测试应用,使用Delphi 10.4自带的Indy 10.6.2.0中的TIdHTTPServer和TIdServerIOHandlerSSLOpenSSL组件,占用本地端口实现C# (.NET 5)客户端与该Win32 Delphi VCL服务端的通信。
我对TLS、SSL和证书了解较少,在开发过程中正在逐步学习,参考的示例让我原本以为这个需求实现起来会很简单。
我使用的.pfx证书属性的“常规”页显示“启用该证书的所有用途”。
我已经引入了OpenSSL 1.0.2.17的DLL文件:
libeay32.dll ssleay32.dll
这些DLL都可以正常加载。
问题现象
目前客户端无法连接到服务端,.NET侧报错“SSL连接无法建立”。
WireShark抓包显示存在“Client Hello”报文(TLS 1.2协议,携带约20种加密套件),但没有返回“Server Hello”、告警或错误报文,实际上仅存在客户端发送的这一条TLS 1.2报文。
客户端携带的加密套件示例:
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:(其他若干加密套件)
我尝试将上述完整加密套件列表赋值给TIdHTTPServer的CipherList属性,但收到“SetCipherList failed”错误。如果留空加密套件列表,组件内部会默认使用"AES:ALL:!aNULL:!eNULL:+RC4:@STRENGTH",我就保持了该默认配置。
配置与代码
组件配置
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
业务代码
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;
调试排查情况
调试时发现证书文件可以通过事件回调提供的密码正常加载,监听线程在收到请求前运行正常。TIdHTTPServer的OnCommandGet事件不会触发。调试中发现InternalReadLn()返回看起来是加密后的二进制数据,后续抛出“解析命令出错”的异常,从返回的数据来看这个报错符合预期。
相关代码片段:
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( ) 返回看起来加密的内容,明确是二进制数据 LInputLine := InternalReadLn(LConn.IOHandler); i := RPos(' ', LInputLine, -1); {Do not Localize} if i = 0 then begin // 该条件成立 raise EIdHTTPErrorParsingCommand.Create(RSHTTPErrorParsingCommand); end;
实际上InternalReadLn()调用的是TIdServerIOHandlerSSLOpenSSL的Readln()方法,返回了上述二进制数据。
相关技术解释提到:
该错误实际来自下面几行代码:
i := RPos(' ', LInputLine, -1); {Do not Localize} if i = 0 then begin raise EIdHTTPErrorParsingCommand.Create(RSHTTPErrorParsingCommand); end;这意味着你的客户端发送了格式错误的HTTP请求。每个请求的首行都必须以HTTP版本号结尾,比如:
GET /login.html HTTP/1.0,所以才会调用RPos()方法。该错误说明该行内容中不存在空格字符。
虽然确实有可能是请求格式错误,但我使用的客户端请求已经经过测试确认是有效可用的。我尝试用浏览器直接访问https://localhost:5500/,也出现了同样的问题。
这让我得出结论:收到“Client Hello”后TLS协商根本没有启动,我不知道原因是什么,也不清楚Indy中哪个部分负责处理TLS协商流程。
我目前主要怀疑以下几个点:
- .pfx证书是否缺少什么必要内容?哪怕别人告诉我这是正常的证书,其属性中已经勾选了“服务器身份验证”选项。
- 要让TLS 1.2正常工作,我需要提供
CertFile、RootCertFile、KeyFile、CipherList中的哪些配置?应该怎么选择合适的配置? - Delphi 10.4 update 2自带的Indy SSL组件是否存在缺陷,或者和OpenSSL 1.0.2q不兼容?
你遇到的核心问题是Indy的TLS协商逻辑没有被触发,IOHandler直接把加密的TLS报文当成明文HTTP内容解析,才会出现解析命令失败的错误,原因是你没有启用TIdHTTPServer的HTTPS支持开关。
具体修复步骤如下:
- 开启HTTPS模式:在
TIdHTTPServer的属性中将UseTLS属性设置为utUseImplicitTLS,这个属性是控制服务端是否在连接建立后直接启动TLS协商的开关,你没有配置它的话,服务端默认会认为所有入站流量都是明文HTTP,不会触发TLS握手逻辑。 - 证书配置修正:你使用PFX格式证书的话,不需要额外拆分证书和密钥文件,只需要将PFX路径同时赋值给
CertFile和KeyFile即可,非双向验证场景不需要填写RootCertFile。 - 加密套件配置:OpenSSL 1.0.2版本默认的加密套件列表已经支持大部分TLS1.2标准套件,不需要手动设置。你之前设置报错是因为直接用了TLS标准的套件名称字符串,而OpenSSL有自己的套件命名规则,保持默认即可。
- 版本兼容性验证:Delphi 10.4自带的Indy 10.6.2完全兼容OpenSSL 1.0.2系列版本,不需要更换版本。
修改完UseTLS属性后,服务端就会在新连接建立后先执行TLS握手,握手成功后再将解密后的HTTP内容交给上层逻辑处理,就能正常触发OnCommandGet事件了。
内容的提问来源于stack exchange,提问作者Mikey

