HTTPS是以安全為目標(biāo)的HTTP通道,簡(jiǎn)單講是HTTP的安全版。即HTTP下加入SSL層,HTTPS的安全基礎(chǔ)是SSL,因此加密的詳細(xì)內(nèi)容就需要SSL。Nebula是一個(gè)為開(kāi)發(fā)者提供一個(gè)快速開(kāi)發(fā)高并發(fā)網(wǎng)絡(luò)服務(wù)程序或搭建高并發(fā)分布式服務(wù)集群的高性能事件驅(qū)動(dòng)網(wǎng)絡(luò)框架。Nebula作為通用網(wǎng)絡(luò)框架提供HTTPS支持十分重要,Nebula既可用作https服務(wù)器,又可用作https客戶(hù)端。本文將結(jié)合Nebula框架的https實(shí)現(xiàn)詳細(xì)講述基于openssl的SSL編程。如果覺(jué)得本文對(duì)你有用,幫忙到Nebula的Github或碼云給個(gè)star,謝謝。Nebula不僅是一個(gè)框架,還提供了一系列基于這個(gè)框架的應(yīng)用,目標(biāo)是打造一個(gè)高性能分布式服務(wù)集群解決方案。Nebula的主要應(yīng)用領(lǐng)域:即時(shí)通訊(成功應(yīng)用于一款I(lǐng)M)、消息推送平臺(tái)、數(shù)據(jù)實(shí)時(shí)分析計(jì)算(成功案例)等,Bwar還計(jì)劃基于Nebula開(kāi)發(fā)爬蟲(chóng)應(yīng)用。
創(chuàng)新互聯(lián)-專(zhuān)業(yè)網(wǎng)站定制、快速模板網(wǎng)站建設(shè)、高性?xún)r(jià)比化德網(wǎng)站開(kāi)發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫(kù),直接使用。一站式化德網(wǎng)站制作公司更省心,省錢(qián),快速模板網(wǎng)站建設(shè)找我們,業(yè)務(wù)覆蓋化德地區(qū)。費(fèi)用合理售后完善,10多年實(shí)體公司更值得信賴(lài)。
HTTPS通信是在TCP通信層與HTTP應(yīng)用層之間增加了SSL層,如果應(yīng)用層不是HTTP協(xié)議也是可以使用SSL加密通信的,比如WebSocket協(xié)議WS的加上SSL層之后的WSS。Nebula框架可以通過(guò)更換Codec達(dá)到不修改代碼變更通訊協(xié)議目的,Nebula增加SSL支持后,所有Nebula支持的通訊協(xié)議都有了SSL加密通訊支持,基于Nebula的業(yè)務(wù)代碼無(wú)須做任何修改。
Socket連接建立后的SSL連接建立過(guò)程:
OpenSSL的API很多,但并不是都會(huì)被使用到,如果需要查看某個(gè)API的詳細(xì)使用方法可以閱讀API文檔。
OpenSSL在使用之前,必須進(jìn)行相應(yīng)的初始化工作。在建立SSL連接之前,要為Client和Server分別指定本次連接采用的協(xié)議及其版本,目前能夠使用的協(xié)議版本包括SSLv2、SSLv3、SSLv2/v3和TLSv1.0。SSL連接若要正常建立,則要求Client和Server必須使用相互兼容的協(xié)議。
下面是Nebula框架SocketChannelSslImpl::SslInit()函數(shù)初始化OpenSSL的代碼,根據(jù)OpenSSL的不同版本調(diào)用了不同的API進(jìn)行初始化。
#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算法庫(kù)函數(shù)( 加載要用到的算法 ),調(diào)用SSL函數(shù)之前必須調(diào)用此函數(shù)
SSL_load_error_strings(); // 錯(cuò)誤信息的初始化
OpenSSL_add_all_algorithms();
#endif
CTX是SSL會(huì)話(huà)環(huán)境,建立連接時(shí)使用不同的協(xié)議,其CTX也不一樣。創(chuàng)建CTX的相關(guān)OpenSSL函數(shù):
//客戶(hù)端、服務(wù)端都需要調(diào)用
SSL_CTX_new(); //申請(qǐng)SSL會(huì)話(huà)環(huán)境
//若有驗(yàn)證對(duì)方證書(shū)的需求,則需調(diào)用
SSL_CTX_set_verify(); //指定證書(shū)驗(yàn)證方式
SSL_CTX_load_verify_location(); //為SSL會(huì)話(huà)環(huán)境加載本應(yīng)用所信任的CA證書(shū)列表
//若有加載證書(shū)的需求,則需調(diào)用
int SSL_CTX_use_certificate_file(); //為SSL會(huì)話(huà)加載本應(yīng)用的證書(shū)
int SSL_CTX_use_certificate_chain_file();//為SSL會(huì)話(huà)加載本應(yīng)用的證書(shū)所屬的證書(shū)鏈
int SSL_CTX_use_PrivateKey_file(); //為SSL會(huì)話(huà)加載本應(yīng)用的私鑰
int SSL_CTX_check_private_key(); //驗(yàn)證所加載的私鑰和證書(shū)是否相匹配
在創(chuàng)建SSL套接字之前要先創(chuàng)建Socket套接字,建立TCP連接。創(chuàng)建SSL套接字相關(guān)函數(shù):
SSL *SSl_new(SSL_CTX *ctx); //創(chuàng)建一個(gè)SSL套接字
int SSL_set_fd(SSL *ssl, int fd); //以讀寫(xiě)模式綁定流套接字
int SSL_set_rfd(SSL *ssl, int fd); //以只讀模式綁定流套接字
int SSL_set_wfd(SSL *ssl, int fd); //以只寫(xiě)模式綁定流套接字
在這一步,我們需要在普通TCP連接的基礎(chǔ)上,建立SSL連接。與普通流套接字建立連接的過(guò)程類(lèi)似:Client使用函數(shù)SSLconnect()【類(lèi)似于流套接字中用的connect()】發(fā)起握手,而Server使用函數(shù)SSL accept()【類(lèi)似于流套接字中用的accept()】對(duì)握手進(jìn)行響應(yīng),從而完成握手過(guò)程。兩函數(shù)原型如下:
int SSL_connect(SSL *ssl);
int SSL_accept(SSL *ssl);
握手過(guò)程完成之后,Client通常會(huì)要求Server發(fā)送證書(shū)信息,以便對(duì)Server進(jìn)行鑒別。其實(shí)現(xiàn)會(huì)用到以下兩個(gè)函數(shù):
X509 *SSL_get_peer_certificate(SSL *ssl); //從SSL套接字中獲取對(duì)方的證書(shū)信息
X509_NAME *X509_get_subject_name(X509 *a); //得到證書(shū)所用者的名字
經(jīng)過(guò)前面的一系列過(guò)程后,就可以進(jìn)行安全的數(shù)據(jù)傳輸了。在數(shù)據(jù)傳輸階段,需要使用SSL_read( )和SSL_write( )來(lái)代替普通流套接字所使用的read( )和write( )函數(shù),以此完成對(duì)SSL套接字的讀寫(xiě)操作,兩個(gè)新函數(shù)的原型分別如下:
int SSL_read(SSL *ssl,void *buf,int num); //從SSL套接字讀取數(shù)據(jù)
int SSL_write(SSL *ssl,const void *buf,int num); //向SSL套接字寫(xiě)入數(shù)據(jù)
當(dāng)Client和Server之間的通信過(guò)程完成后,就使用以下函數(shù)來(lái)釋放前面過(guò)程中申請(qǐng)的SSL資源:
int SSL_shutdown(SSL *ssl); //關(guān)閉SSL套接字
void SSl_free(SSL *ssl); //釋放SSL套接字
void SSL_CTX_free(SSL_CTX *ctx); //釋放SSL會(huì)話(huà)環(huán)境
HTTPS 使用 SSL(Secure Socket Layer) 和 TLS(Transport LayerSecurity)這兩個(gè)協(xié)議。
SSL 技術(shù)最初是由瀏覽器開(kāi)發(fā)商網(wǎng)景通信公司率先倡導(dǎo)的,開(kāi)發(fā)過(guò) SSL3.0之前的版本。目前主導(dǎo)權(quán)已轉(zhuǎn)移到 IETF(Internet Engineering Task Force,Internet 工程任務(wù)組)的手中。
IETF 以 SSL3.0 為基準(zhǔn),后又制定了 TLS1.0、TLS1.1 和 TLS1.2。TSL 是以SSL 為原型開(kāi)發(fā)的協(xié)議,有時(shí)會(huì)統(tǒng)一稱(chēng)該協(xié)議為 SSL。當(dāng)前主流的版本是SSL3.0 和 TLS1.0。
由于 SSL1.0 協(xié)議在設(shè)計(jì)之初被發(fā)現(xiàn)出了問(wèn)題,就沒(méi)有實(shí)際投入使用。SSL2.0 也被發(fā)現(xiàn)存在問(wèn)題,所以很多瀏覽器直接廢除了該協(xié)議版本。
Nebula框架同時(shí)支持SSL服務(wù)端應(yīng)用和SSL客戶(hù)端應(yīng)用,對(duì)openssl的初始化只需要初始化一次即可(SslInit()只需調(diào)用一次)。Nebula框架的SSL相關(guān)代碼(包括客戶(hù)端和服務(wù)端的實(shí)現(xiàn))都封裝在SocketChannelSslImpl這個(gè)類(lèi)中。Nebula的SSL通信是基于異步非阻塞的socket通信,并且不使用openssl的BIO(因?yàn)闆](méi)有必要,代碼還更復(fù)雜了)。
SocketChannelSslImpl是SocketChannelImpl的派生類(lèi),在SocketChannelImpl常規(guī)TCP通信之上增加了SSL通信層,兩個(gè)類(lèi)的調(diào)用幾乎沒(méi)有差異。SocketChannelSslImpl類(lèi)聲明如下:
```C++
class SocketChannelSslImpl : public SocketChannelImpl
{
public:
SocketChannelSslImpl(SocketChannel* pSocketChannel, std::shared_ptr
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;
// 覆蓋基類(lèi)的Send()方法,實(shí)現(xiàn)非阻塞socket連接建立后繼續(xù)建立SSL連接,并收發(fā)數(shù)據(jù)
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; //在基類(lèi)m_ucChannelStatus通道狀態(tài)基礎(chǔ)上增加SSL通道狀態(tài)
bool m_bIsClientConnection;
SSL* m_pSslConnection;
static SSL_CTX* m_pServerSslCtx; //當(dāng)打開(kāi)ssl選項(xiàng)編譯,啟動(dòng)Nebula服務(wù)則自動(dòng)創(chuàng)建
static SSL_CTX* m_pClientSslCtx; //默認(rèn)為空,當(dāng)打開(kāi)ssl選項(xiàng)編譯并且第一次發(fā)起了對(duì)其他SSL服務(wù)的連接時(shí)(比如訪(fǎng)問(wèn)一個(gè)https地址)創(chuàng)建
};
SocketChannelSslImpl類(lèi)中帶override關(guān)鍵字的方法都是覆蓋基類(lèi)SocketChannelImpl的同名方法,也是實(shí)現(xiàn)SSL通信與非SSL通信調(diào)用透明的關(guān)鍵。不帶override關(guān)鍵字的方法都是SSL通信相關(guān)方法,這些方法里有openssl的函數(shù)調(diào)用。不帶override的方法中有靜態(tài)和非靜態(tài)之分,靜態(tài)方法在進(jìn)程中只會(huì)被調(diào)用一次,與具體Channel對(duì)象無(wú)關(guān)。SocketChannel外部不需要調(diào)用非靜態(tài)的ssl相關(guān)方法。
因?yàn)槭欠亲枞膕ocket,SSL_do_handshake()和SSL_write()、SSL_read()返回值并不完全能判斷是否出錯(cuò),還需要SSL_get_error()獲取錯(cuò)誤碼。SSL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE都是正常的。
網(wǎng)上的大部分openssl例子程序是按順序調(diào)用openssl函數(shù)簡(jiǎn)單實(shí)現(xiàn)同步ssl通信,在非阻塞IO應(yīng)用中,ssl通信要復(fù)雜許多。SocketChannelSslImpl實(shí)現(xiàn)的是非阻塞的ssl通信,從該類(lèi)的實(shí)現(xiàn)上看整個(gè)通信過(guò)程并非完全線(xiàn)性的。下面的SSL通信圖更清晰地說(shuō)明了Nebula框架中SSL通信是如何實(shí)現(xiàn)的:
![Nebula_ssl](https://oscimg.oschina.net/oscnet/e6f58a56d4fbcf9dbc3c4a1d9c3fdbb970e.jpg "Nebula_ssl")
SocketChannelSslImpl中的靜態(tài)方法在進(jìn)程生命期內(nèi)只需調(diào)用一次,也可以理解成SSL_CTX_new()、SSL_CTX_free()等方法只需調(diào)用一次。更進(jìn)一步理解SSL_CTX結(jié)構(gòu)體在進(jìn)程內(nèi)只需要?jiǎng)?chuàng)建一次(在Nebula中分別為Server和Client各創(chuàng)建一個(gè))就可以為所有SSL連接所用;當(dāng)然,為每個(gè)SSL連接創(chuàng)建獨(dú)立的SSL_CTX也沒(méi)問(wèn)題(Nebula 0.4中實(shí)測(cè)過(guò)為每個(gè)Client創(chuàng)建獨(dú)立的SSL_CTX),但一般不這么做,因?yàn)檫@樣會(huì)消耗更多的內(nèi)存資源,并且效率也會(huì)更低。
建立SSL連接時(shí),客戶(hù)端調(diào)用SSL_connect(),服務(wù)端調(diào)用SSL_accept(),許多openssl的demo都是這么用的。Nebula中用的是SSL_do_handshake(),這個(gè)方法同時(shí)適用于客戶(hù)端和服務(wù)端,在兼具client和server功能的服務(wù)更適合用SSL_do_handshake()。注意調(diào)用SSL_do_handshake()前,如果是client端需要先調(diào)用SSL_set_connect_state(),如果是server端則需要先調(diào)用SSL_set_accept_state()。非阻塞IO中,SSL_do_handshake()可能需要調(diào)用多次才能完成握手,具體調(diào)用時(shí)機(jī)需根據(jù)SSL_get_error()獲取錯(cuò)誤碼SSL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE判斷需監(jiān)聽(tīng)讀事件還是寫(xiě)事件,在對(duì)應(yīng)事件觸發(fā)時(shí)再次調(diào)用SSL_do_handshake()。詳細(xì)實(shí)現(xiàn)請(qǐng)參考SocketChannelSslImpl的Send和Recv方法。
關(guān)閉SSL連接時(shí)先調(diào)用SSL_shutdown()正常關(guān)閉SSL層連接(非阻塞IO中SSL_shutdown()亦可能需要調(diào)用多次)再調(diào)用SSL_free()釋放SSL連接資源,最后關(guān)閉socket連接。SSL_CTX無(wú)須釋放。整個(gè)SSL通信順利完成,Nebula 0.4在開(kāi)多個(gè)終端用shell腳本死循環(huán)調(diào)用curl簡(jiǎn)單壓測(cè)中SSL client和SSL server功能一切正常:
```bash
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
測(cè)試方法如下圖:
查看資源使用情況,SSL Server端的內(nèi)存使用一直在增長(zhǎng),疑似有內(nèi)存泄漏,不過(guò)pmap -d查看某一項(xiàng)anon內(nèi)存達(dá)到近18MB時(shí)不再增長(zhǎng),說(shuō)明可能不是內(nèi)存泄漏,只是部分內(nèi)存被openssl當(dāng)作cache使用了。這個(gè)問(wèn)題網(wǎng)上沒(méi)找到解決辦法。從struct ssl_ctx_st結(jié)構(gòu)體定義發(fā)現(xiàn)端倪,再?gòu)膎ginx源碼中發(fā)現(xiàn)了SSL_CTX_remove_session(),于是在SSL_free()之前加上SSL_CTX_remove_session()。session復(fù)用可以提高SSL通信效率,不過(guò)Nebula暫時(shí)不需要。
這種測(cè)試方法把NebulaInterface作為SSL服務(wù)端,NebulaLogic作為SSL客戶(hù)端,同時(shí)完成了Nebula框架SSL服務(wù)端和客戶(hù)端功能測(cè)試,簡(jiǎn)單的壓力測(cè)試。Nebula框架的SSL通信測(cè)試通過(guò),也可以投入生產(chǎn)應(yīng)用,在后續(xù)應(yīng)用中肯定還會(huì)繼續(xù)完善。openssl真的難用,難怪被吐槽那么多,或許不久之后的Nebula版本將用其他ssl庫(kù)替換掉openssl。
加上SSL支持的Nebula框架測(cè)試通過(guò),雖然不算太復(fù)雜,但過(guò)程還是蠻曲折,耗時(shí)也挺長(zhǎng)。這里把Nebula使用openssl開(kāi)發(fā)SSL通信分享出來(lái),希望對(duì)準(zhǔn)備使用openssl的開(kāi)發(fā)者有用。如果覺(jué)得本文對(duì)你有用,別忘了到Nebula的Github或碼云給個(gè)star,謝謝。
參考資料: