在Visual C++中无需OpenSSL获取SMTP服务器(587端口,STARTTLS)证书
不用OpenSSL,用WinHTTP实现SMTP STARTTLS获取证书
没问题,完全可以不依赖OpenSSL,用WinHTTP原生API就能搞定SMTP 587端口的STARTTLS流程并获取服务器证书。和你之前处理隐式SSL(465端口)的流程不同,STARTTLS需要先建立明文连接,再手动触发TLS升级,具体步骤和代码如下:
核心实现思路
- 建立明文连接:先以非SSL模式连接到SMTP 587端口,不设置
WINHTTP_FLAG_SECURE标记 - 协商STARTTLS支持:发送
EHLO命令,确认服务器支持STARTTLS扩展 - 触发TLS升级:发送
STARTTLS命令,收到服务器确认后,调用WinHTTP的升级接口切换到SSL/TLS连接 - 获取证书:升级完成后,再用
WinHttpQueryOption获取服务器证书上下文
完整代码示例
#include <windows.h> #include <winhttp.h> #include <wincrypt.h> #include <iostream> #include <string> #pragma comment(lib, "winhttp.lib") #pragma comment(lib, "crypt32.lib") int main() { const char* szServer = "smtp.gmail.com"; const int PortNum = 587; // Convert server name to wide string USES_CONVERSION; LPWSTR lpServerName = A2W(szServer); BOOL bResults = FALSE; HINTERNET hSession = NULL, hConnect = NULL, hRequest = NULL; // 1. 创建WinHTTP会话 hSession = WinHttpOpen(L"SMTP STARTTLS Client", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); if (!hSession) { std::cerr << "WinHttpOpen failed: " << GetLastError() << std::endl; goto Cleanup; } // 2. 建立明文连接(不使用SSL标记) hConnect = WinHttpConnect(hSession, lpServerName, PortNum, 0); if (!hConnect) { std::cerr << "WinHttpConnect failed: " << GetLastError() << std::endl; goto Cleanup; } // 3. 创建明文请求 hRequest = WinHttpOpenRequest(hConnect, L"POST", NULL, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0); // 注意这里不要加WINHTTP_FLAG_SECURE if (!hRequest) { std::cerr << "WinHttpOpenRequest failed: " << GetLastError() << std::endl; goto Cleanup; } // 4. 发送EHLO命令,协商STARTTLS支持 std::string ehloCmd = "EHLO localhost\r\n"; bResults = WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, (LPVOID)ehloCmd.c_str(), ehloCmd.length(), ehloCmd.length(), 0); if (!bResults) { std::cerr << "WinHttpSendRequest (EHLO) failed: " << GetLastError() << std::endl; goto Cleanup; } // 接收EHLO响应,确认服务器支持STARTTLS bResults = WinHttpReceiveResponse(hRequest, NULL); if (!bResults) { std::cerr << "WinHttpReceiveResponse (EHLO) failed: " << GetLastError() << std::endl; goto Cleanup; } // 读取响应内容(简单检查是否包含STARTTLS) char buffer[1024]; DWORD bytesRead = 0; std::string ehloResponse; while (WinHttpReadData(hRequest, buffer, sizeof(buffer)-1, &bytesRead) && bytesRead > 0) { buffer[bytesRead] = '\0'; ehloResponse += buffer; } if (ehloResponse.find("STARTTLS") == std::string::npos) { std::cerr << "Server does not support STARTTLS" << std::endl; goto Cleanup; } // 5. 发送STARTTLS命令 std::string starttlsCmd = "STARTTLS\r\n"; bResults = WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, (LPVOID)starttlsCmd.c_str(), starttlsCmd.length(), starttlsCmd.length(), 0); if (!bResults) { std::cerr << "WinHttpSendRequest (STARTTLS) failed: " << GetLastError() << std::endl; goto Cleanup; } // 接收STARTTLS响应(服务器应返回220 Ready to start TLS) bResults = WinHttpReceiveResponse(hRequest, NULL); if (!bResults) { std::cerr << "WinHttpReceiveResponse (STARTTLS) failed: " << GetLastError() << std::endl; goto Cleanup; } // 6. 升级连接到SSL/TLS DWORD dwOptionValue = TRUE; bResults = WinHttpSetOption(hRequest, WINHTTP_OPTION_UPGRADE_TO_SECURE, &dwOptionValue, sizeof(dwOptionValue)); if (!bResults) { std::cerr << "WinHttpSetOption (UPGRADE_TO_SECURE) failed: " << GetLastError() << std::endl; goto Cleanup; } // 7. 获取服务器证书上下文 PCCERT_CONTEXT pCert = NULL; DWORD dwSize = sizeof(pCert); BOOL res = WinHttpQueryOption(hRequest, WINHTTP_OPTION_SERVER_CERT_CONTEXT, &pCert, &dwSize); if (res && pCert) { std::cout << "Successfully retrieved server certificate!" << std::endl; // 这里可以对证书做处理,比如打印证书信息 // 记得用完后释放证书上下文 CertFreeCertificateContext(pCert); } else { std::cerr << "WinHttpQueryOption (CERT_CONTEXT) failed: " << GetLastError() << std::endl; } Cleanup: // 释放WinHTTP句柄 if (hRequest) WinHttpCloseHandle(hRequest); if (hConnect) WinHttpCloseHandle(hConnect); if (hSession) WinHttpCloseHandle(hSession); return 0; }
关键注意事项
- 明文连接初始化:创建请求时不要加
WINHTTP_FLAG_SECURE,否则会直接尝试SSL握手,不符合STARTTLS流程 - SMTP命令格式:所有SMTP命令必须以
\r\n结尾,这是SMTP协议的强制要求 - 证书释放:获取到
PCCERT_CONTEXT后,一定要调用CertFreeCertificateContext释放资源,避免内存泄漏 - 错误处理:每一步WinHTTP调用都要检查返回值,根据
GetLastError()排查问题
内容的提问来源于stack exchange,提问作者steve_79
相关产品推荐
相关产品推荐

