HTTPS通信的C++實現

HTTPS是以安全為目標的HTTP通道,簡單講是HTTP的安全版。即HTTP下加入SSL層,HTTPS的安全基礎是SSL,因此加密的詳細內容就需要SSL。Nebula是一個為開發者提供一個快速開發高併發網絡服務程序或搭建高併發分佈式服務集群的高性能事件驅動網絡框架。Nebula作為通用網絡框架提供HTTPS支持十分重要,Nebula既可用作https服務器,又可用作https客戶端。本文將結合Nebula框架的https實現詳細講述基於openssl的SSL編程。如果覺得本文對你有用,幫忙到Nebula的

Github碼雲給個star,謝謝。Nebula不僅是一個框架,還提供了一系列基於這個框架的應用,目標是打造一個高性能分佈式服務集群解決方案。Nebula的主要應用領域:即時通訊(成功應用於一款IM)、消息推送平臺、數據實時分析計算(成功案例)等,Bwar還計劃基於Nebula開發爬蟲應用。1. SSL加密通信

HTTPS通信是在TCP通信層與HTTP應用層之間增加了SSL層,如果應用層不是HTTP協議也是可以使用SSL加密通信的,比如WebSocket協議WS的加上SSL層之後的WSS。Nebula框架可以通過更換Codec達到不修改代碼變更通訊協議目的,Nebula增加SSL支持後,所有Nebula支持的通訊協議都有了SSL加密通訊支持,基於Nebula的業務代碼無須做任何修改。

HTTPS通信的C++實現

Socket連接建立後的SSL連接建立過程:

HTTPS通信的C++實現

2. OpenSSL API

OpenSSL的API很多,但並不是都會被使用到,如果需要查看某個API的詳細使用方法可以閱讀API文檔。

2.1 初始化OpenSSL

OpenSSL在使用之前,必須進行相應的初始化工作。在建立SSL連接之前,要為Client和Server分別指定本次連接採用的協議及其版本,目前能夠使用的協議版本包括SSLv2、SSLv3、SSLv2/v3和TLSv1.0。SSL連接若要正常建立,則要求Client和Server必須使用相互兼容的協議。 下面是Nebula框架SocketChannelSslImpl::SslInit()函數初始化OpenSSL的代碼,根據OpenSSL的不同版本調用了不同的API進行初始化。

#if OPENSSL_VERSION_NUMBER >= 0x10100003L
if (OPENSSL_init_ssl(OPENSSL_INIT_LOAD_CONFIG, NULL) == 0)
{
pLogger->WriteLog(neb::Logger::ERROR, __FILE__, __LINE__, __FUNCTION__, "OPENSSL_init_ssl() failed!");
return(ERR_SSL_INIT);
}
/*
* OPENSSL_init_ssl() may leave errors in the error queue
* while returning success
*/
ERR_clear_error();
#else
OPENSSL_config(NULL);
SSL_library_init(); // 初始化SSL算法庫函數( 加載要用到的算法 ),調用SSL函數之前必須調用此函數
SSL_load_error_strings(); // 錯誤信息的初始化
OpenSSL_add_all_algorithms();
#endif

2.2 創建CTX

CTX是SSL會話環境,建立連接時使用不同的協議,其CTX也不一樣。創建CTX的相關OpenSSL函數:

//客戶端、服務端都需要調用
SSL_CTX_new(); //申請SSL會話環境
//若有驗證對方證書的需求,則需調用
SSL_CTX_set_verify(); //指定證書驗證方式
SSL_CTX_load_verify_location(); //為SSL會話環境加載本應用所信任的CA證書列表
//若有加載證書的需求,則需調用
int SSL_CTX_use_certificate_file(); //為SSL會話加載本應用的證書
int SSL_CTX_use_certificate_chain_file();//為SSL會話加載本應用的證書所屬的證書鏈
int SSL_CTX_use_PrivateKey_file(); //為SSL會話加載本應用的私鑰
int SSL_CTX_check_private_key(); //驗證所加載的私鑰和證書是否相匹配

2.3 創建SSL套接字

在創建SSL套接字之前要先創建Socket套接字,建立TCP連接。創建SSL套接字相關函數:

SSL *SSl_new(SSL_CTX *ctx); //創建一個SSL套接字
int SSL_set_fd(SSL *ssl, int fd); //以讀寫模式綁定流套接字
int SSL_set_rfd(SSL *ssl, int fd); //以只讀模式綁定流套接字
int SSL_set_wfd(SSL *ssl, int fd); //以只寫模式綁定流套接字

2.4 完成SSL握手

在這一步,我們需要在普通TCP連接的基礎上,建立SSL連接。與普通流套接字建立連接的過程類似:Client使用函數SSL_connect()【類似於流套接字中用的connect()】發起握手,而Server使用函數SSL_ accept()【類似於流套接字中用的accept()】對握手進行響應,從而完成握手過程。兩函數原型如下:

int SSL_connect(SSL *ssl);
int SSL_accept(SSL *ssl);

握手過程完成之後,Client通常會要求Server發送證書信息,以便對Server進行鑑別。其實現會用到以下兩個函數:

X509 *SSL_get_peer_certificate(SSL *ssl); //從SSL套接字中獲取對方的證書信息
X509_NAME *X509_get_subject_name(X509 *a); //得到證書所用者的名字

2.5 數據傳輸

經過前面的一系列過程後,就可以進行安全的數據傳輸了。在數據傳輸階段,需要使用SSL_read( )和SSL_write( )來代替普通流套接字所使用的read( )和write( )函數,以此完成對SSL套接字的讀寫操作,兩個新函數的原型分別如下:

int SSL_read(SSL *ssl,void *buf,int num); //從SSL套接字讀取數據
int SSL_write(SSL *ssl,const void *buf,int num); //向SSL套接字寫入數據

2.6 會話結束

當Client和Server之間的通信過程完成後,就使用以下函數來釋放前面過程中申請的SSL資源:

int SSL_shutdown(SSL *ssl); //關閉SSL套接字
void SSl_free(SSL *ssl); //釋放SSL套接字
void SSL_CTX_free(SSL_CTX *ctx); //釋放SSL會話環境

3. SSL 和 TLS

HTTPS 使用 SSL(Secure Socket Layer) 和 TLS(Transport LayerSecurity)這兩個協議。 SSL 技術最初是由瀏覽器開發商網景通信公司率先倡導的,開發過 SSL3.0之前的版本。目前主導權已轉移到 IETF(Internet Engineering Task Force,Internet 工程任務組)的手中。

IETF 以 SSL3.0 為基準,後又制定了 TLS1.0、TLS1.1 和 TLS1.2。TSL 是以SSL 為原型開發的協議,有時會統一稱該協議為 SSL。當前主流的版本是SSL3.0 和 TLS1.0。

由於 SSL1.0 協議在設計之初被發現出了問題,就沒有實際投入使用。SSL2.0 也被發現存在問題,所以很多瀏覽器直接廢除了該協議版本。

4. Nebula中的SSL通訊實現

Nebula框架同時支持SSL服務端應用和SSL客戶端應用,對openssl的初始化只需要初始化一次即可(SslInit()只需調用一次)。Nebula框架的SSL相關代碼(包括客戶端和服務端的實現)都封裝在SocketChannelSslImpl這個類中。Nebula的SSL通信是基於異步非阻塞的socket通信,並且不使用openssl的BIO(因為沒有必要,代碼還更復雜了)。

SocketChannelSslImpl是SocketChannelImpl的派生類,在SocketChannelImpl常規TCP通信之上增加了SSL通信層,兩個類的調用幾乎沒有差異。SocketChannelSslImpl類聲明如下:

class SocketChannelSslImpl : public SocketChannelImpl
{
public:
SocketChannelSslImpl(SocketChannel* pSocketChannel, std::shared_ptr pLogger, int iFd, uint32 ulSeq, ev_tstamp dKeepAlive = 0.0);
virtual ~SocketChannelSslImpl();
static int SslInit(std::shared_ptr pLogger);
static int SslServerCtxCreate(std::shared_ptr pLogger);
static int SslServerCertificate(std::shared_ptr pLogger,
const std::string& strCertFile, const std::string& strKeyFile);
static void SslFree();
int SslClientCtxCreate();
int SslCreateConnection();
int SslHandshake();
int SslShutdown();
virtual bool Init(E_CODEC_TYPE eCodecType, bool bIsClient = false) override;
// 覆蓋基類的Send()方法,實現非阻塞socket連接建立後繼續建立SSL連接,並收發數據
virtual E_CODEC_STATUS Send() override;
virtual E_CODEC_STATUS Send(int32 iCmd, uint32 uiSeq, const MsgBody& oMsgBody) override;
virtual E_CODEC_STATUS Send(const HttpMsg& oHttpMsg, uint32 ulStepSeq) override;
virtual E_CODEC_STATUS Recv(MsgHead& oMsgHead, MsgBody& oMsgBody) override;
virtual E_CODEC_STATUS Recv(HttpMsg& oHttpMsg) override;
virtual E_CODEC_STATUS Recv(MsgHead& oMsgHead, MsgBody& oMsgBody, HttpMsg& oHttpMsg) override;
virtual bool Close() override;
protected:
virtual int Write(CBuffer* pBuff, int& iErrno) override;
virtual int Read(CBuffer* pBuff, int& iErrno) override;
private:
E_SSL_CHANNEL_STATUS m_eSslChannelStatus; //在基類m_ucChannelStatus通道狀態基礎上增加SSL通道狀態
bool m_bIsClientConnection;
SSL* m_pSslConnection;
static SSL_CTX* m_pServerSslCtx; //當打開ssl選項編譯,啟動Nebula服務則自動創建
static SSL_CTX* m_pClientSslCtx; //默認為空,當打開ssl選項編譯並且第一次發起了對其他SSL服務的連接時(比如訪問一個https地址)創建

};

SocketChannelSslImpl類中帶override關鍵字的方法都是覆蓋基類SocketChannelImpl的同名方法,也是實現SSL通信與非SSL通信調用透明的關鍵。不帶override關鍵字的方法都是SSL通信相關方法,這些方法裡有openssl的函數調用。不帶override的方法中有靜態和非靜態之分,靜態方法在進程中只會被調用一次,與具體Channel對象無關。SocketChannel外部不需要調用非靜態的ssl相關方法。

因為是非阻塞的socket,SSL_do_handshake()和SSL_write()、SSL_read()返回值並不完全能判斷是否出錯,還需要SSL_get_error()獲取錯誤碼。SSL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE都是正常的。

網上的大部分openssl例子程序是按順序調用openssl函數簡單實現同步ssl通信,在非阻塞IO應用中,ssl通信要複雜許多。SocketChannelSslImpl實現的是非阻塞的ssl通信,從該類的實現上看整個通信過程並非完全線性的。下面的SSL通信圖更清晰地說明了Nebula框架中SSL通信是如何實現的:

HTTPS通信的C++實現

SocketChannelSslImpl中的靜態方法在進程生命期內只需調用一次,也可以理解成SSL_CTX_new()、SSL_CTX_free()等方法只需調用一次。更進一步理解SSL_CTX結構體在進程內只需要創建一次(在Nebula中分別為Server和Client各創建一個)就可以為所有SSL連接所用;當然,為每個SSL連接創建獨立的SSL_CTX也沒問題(Nebula 0.4中實測過為每個Client創建獨立的SSL_CTX),但一般不這麼做,因為這樣會消耗更多的內存資源,並且效率也會更低。

建立SSL連接時,客戶端調用SSL_connect(),服務端調用SSL_accept(),許多openssl的demo都是這麼用的。Nebula中用的是SSL_do_handshake(),這個方法同時適用於客戶端和服務端,在兼具client和server功能的服務更適合用SSL_do_handshake()。注意調用SSL_do_handshake()前,如果是client端需要先調用SSL_set_connect_state(),如果是server端則需要先調用SSL_set_accept_state()。非阻塞IO中,SSL_do_handshake()可能需要調用多次才能完成握手,具體調用時機需根據SSL_get_error()獲取錯誤碼SSL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE判斷需監聽讀事件還是寫事件,在對應事件觸發時再次調用SSL_do_handshake()。詳細實現請參考SocketChannelSslImpl的Send和Recv方法。

關閉SSL連接時先調用SSL_shutdown()正常關閉SSL層連接(非阻塞IO中SSL_shutdown()亦可能需要調用多次)再調用SSL_free()釋放SSL連接資源,最後關閉socket連接。SSL_CTX無須釋放。整個SSL通信順利完成,Nebula 0.4在開多個終端用shell腳本死循環調用curl簡單壓測中SSL client和SSL server功能一切正常:

while :
do
curl -v -k -H "Content-Type:application/json" -X POST -d '{"hello":"nebula ssl test"}' https://192.168.157.168:16003/test_ssl

done

測試方法如下圖:

HTTPS通信的C++實現

查看資源使用情況,SSL Server端的內存使用一直在增長,疑似有內存洩漏,不過pmap -d查看某一項anon內存達到近18MB時不再增長,說明可能不是內存洩漏,只是部分內存被openssl當作cache使用了。這個問題網上沒找到解決辦法。從struct ssl_ctx_st結構體定義發現端倪,再從nginx源碼中發現了SSL_CTX_remove_session(),於是在SSL_free()之前加上SSL_CTX_remove_session()。session複用可以提高SSL通信效率,不過Nebula暫時不需要。

這種測試方法把NebulaInterface作為SSL服務端,NebulaLogic作為SSL客戶端,同時完成了Nebula框架SSL服務端和客戶端功能測試,簡單的壓力測試。Nebula框架的SSL通信測試通過,也可以投入生產應用,在後續應用中肯定還會繼續完善。openssl真的難用,難怪被吐槽那麼多,或許不久之後的Nebula版本將用其他ssl庫替換掉openssl。

5. 結束

加上SSL支持的Nebula框架測試通過,雖然不算太複雜,但過程還是蠻曲折,耗時也挺長。這裡把Nebula使用openssl開發SSL通信分享出來,希望對準備使用openssl的開發者有用。如果覺得本文對你有用,別忘了到Nebula的Github碼雲給個star,謝謝。


分享到:


相關文章: