使用openssl進行加密通信時,通常是先建立socket連接,然后使用SSL_XXX系列函數在普通socket之上建立安全連接,然后發送和接收數據。openssl的這些函數可以支持底層的socket是非阻塞模式的。但當將openssl和libuv進行結合時,會遇到一些問題:
- openssl在進行數據讀寫之前,需要進行若干次“握手”。“握手”中會有若干次的數據讀寫。這個在普通的socket連接中是沒有的,在libuv的回調函數中需要進行處理。
- 由於openssl需要對數據進行加密和解密,當openssl讀數據的時候,有可能會出現雖然加密的數據已經全部接收到本地了,但仍需要和遠端進行通信來進一步確認如何解密數據。(會不會出現這個過程不太確定。。。網上有文章說可能會出現,我也沒有仔細研究過openssl的實現細節,所有寧可信其有,不可信其無吧。。。)
解決這兩個問題的思路是一樣的,將openssl看做是一個數據過濾器,可參考這篇文章。
在和libuv結合時,openssl不能直接對socket進行讀寫,因為對socket的讀寫操作已經被libuv完全封裝了。不過openssl可以通過BIO進行讀寫數據。也就是說,需要准備兩個BIO,一個用於存儲openssl加密好的數據,一個用於存儲接收到的加密數據以備openssl解密。這個操作直接調用下面這個函數即可完成:
void SSL_set_bio(SSL *ssl, BIO *rbio, BIO *wbio);
設置好這兩個BIO之后,SSL_XXX系列函數的所有操作都是針對這兩個BIO,不再直接和socket打交道。這樣對socket的操作就可以委托給libuv了。
對於寫數據到socket,直接將數據丟給libuv就可以了。但讀數據的時候會略微麻煩一些。在創建安全連接的時候openssl需要多次“握手”操作,也就是需要朝socket讀寫幾次數據。這個過程需要在libuv的read_cb函數里處理。也就是說在libuv的read_cb函數需要區分要讀的數據是“握手”時的數據還是真正通信讀取的數據。這個判斷通過
int SSL_is_init_finished(SSL *ssl);
函數實現,也就是判斷openssl是否完成了安全連接的初始化。
對於前面提到的第二個問題,openssl提供了解決這個問題的機制。SSL_XXX系列函數的返回值可以通過
int SSL_get_error(const SSL *ssl, int ret);
來獲取其具體的含義,其中兩個重要的返回結果是SSL_ERROR_WANT_READ和SSL_ERROR_WANT_WRITE。在調用SSL_connect,SSL_read和SSL_write時,openssl可能需要讀取更多的數據或者發送數據,這兩個返回值表明openssl的意圖。注意:這三個函數都有可能返回這兩個值。也就是說在讀數據的時候可能需要寫數據,在寫數據的時候可能需要讀數據。
啰啰嗦嗦說了這么多,上代碼才是王道。以下代碼只是示意,並不能直接編譯運行^v^。 首先,聲明變量:
SSL *ssl;
SSL_ctx *ssl_ctx;
BIO *read_bio;
BIO *write_bio;
uv_tcp_t *con
在libuv的on_connect_cb函數中初始化openssl並開始“握手”。
void on_connect_cb(uv_connect_t *req, int status) { //設置數據讀取的回調函數 uv_read_start((uv_stream_t*)con, on_alloc_cb, on_read_cb); ssl = SSL_new(ssl_ctx); read_bio = BIO_new(BIO_s_mem()); write_bio = BIO_new(BIO_s_mem()); SSL_set_bio(ssl, read_bio, write_bio); SSL_set_connect_state(ssl); // 這是個客戶端連接 int ret = SSL_connect(ssl); // 開始握手。這個函數僅僅是將數據寫如了BIO緩存,並沒有發送到socket上。 write_bio_to_socket(); // 如果有,將wirte BIO中的數據寫入socket。(具體定義見后面代碼) if (ret != 1) { // connect出錯了,看看具體什么問題。 int err = SSL_get_error(ssl, ret); if (err == SSL_ERROR_WANT_READ) { // 在read回調函數中讀取數據 } else if (err == SSL_ERROR_WANT_WRITE) { write_bio_to_socket(); // 將write BIO中的數據發送出去 } } }
真正的重頭戲是在on_read_cb中。
void read_cb(uv_stream_t* stream, ssize_t nread, const uv_buf_t *buf) { if (nread == UV_EOF) { // 已經讀完了所有的數據 read_data_after_handshake(); return; } else { // 讀取數據到BIO中。buf中的數據是加密數據,將其放到BIO中,讓openssl將其解碼。 BIO_write(read_bio, buf -> base, nread); if (!SSL_is_init_finished(ssl)) { // 我們還沒有完成ssl的初始化,繼續進行握手。 int ret = SSL_connect(ssl); write_bio_to_socket(); if (ret != 1) { int err = SSL_get_error(ssl, ret); if (err == SSL_ERROR_WANT_READ) { // 在read回調函數中讀取數據 } else if (err == SSL_ERROR_WANT_WRITE) { write_bio_to_socket(); } } else { // 握手完成,發送數據。 send_data_after_handshake(); } } else { // ssl已經初始化好了, 我們可以從BIO中讀取已經解密的數據。 read_data_after_handshake(); } } free(buf -> base); }
下面來看看write_bio_to_socket()的實現,這個函數很簡單,就是將write_bio中的數據丟給libuv進行發送。
void write_bio_to_socket() { char buf[1024]; int hasread = BIO_read(write_bio, buf, sizeof(buf)); if (hasread <= 0) { // 無數據可寫。 return; } uv_write_t *wreq = (uv_write_t*)malloc(sizeof(uv_write_t)); char *tmp = malloc(hasread); memcpy(tmp, buf, hasread); uv_buf_t *bufs = (uv_buf_t*)malloc(sizeof(uv_buf_t) * 1); bufs[0].base = tmp; bufs[0].len = hasread; uv_write(wreq, (uv_stream_t*)con, bufs, 1, on_write_cb); // 記得在on_write_cb中釋放這里分配的內存。 }
BIO_read有可能一次讀取不完write_bio中的數據,所以這個地方需要一個循環多次調用BIO_read直到數據全部讀完。這里為了簡單就只讀一次了^v^。
send_data_after_handshake函數也很簡單,就是將需要發送的數據寫入wirte_bio中然后丟給libuv發送,還需要處理有數據要讀取的情況。
void send_data_after_handshake() { int ret = SSL_write(ssl, data, data_len); // data中存放了要發送的數據 if (ret > 0) { // 寫入socket write_bio_to_socket(); } else if (ret == 0) { // 連接關閉了?? uv_close((uv_handle_t*)con, on_close_cb); } else { // 需要讀取或寫入數據。 int err = SSL_get_error(client -> ssl, ret); if (err == SSL_ERROR_WANT_READ) { // 在read回調中處理(其實如果有數據要讀時什么都不要,等read回調就行了。。。) } else if (err == SSL_ERROR_WANT_WRITE) { write_bio_to_socket(); } } }
最后是read_data_after_handshake,這個函數將openlls解密好的數據讀取出來,同時還需要處理在讀取數據的時候需要寫入數據的問題。
void read_data_after_handshake() { char buf[1024]; memset(buf, '\0', sizeof(buf)); int ret = SSL_read(ssl, buf, sizeof(buf)); if (ret < 0) { int err = SSL_get_error(client -> ssl, ret); if (err == SSL_ERROR_WANT_READ) { // 在read回調函數中讀取數據 } else if (err == SSL_ERROR_WANT_WRITE) { // 有數據要寫,將write BIO中的數據發送出去 write_bio_to_socket(); } } // 解密好的數據就存放在buf中了。當然,這個地方也可能需要多次調用SSL_read來講所有數據都讀出來。 }
以上就是全部的示例代碼了。
關於這個openssl和libuv結合使用的思路還沒有進行嚴格的測試,我也只是在工程中初步測試了一下可以走通。對於一些細節的處理還不是很到位。這里只是提供了一個libuv和openssl結合的思路,如果有任何問題,歡迎指正。^v^~