在libuv中使用openssl創建ssl鏈接

在libuv中使用openssl創建ssl鏈接

@(blogs)
使用openssl進行加密通訊時,一般是先創建socket鏈接,而後使用SSL_XXX系列函數在普通socket之上創建安全鏈接,而後發送和接收數據。openssl的這些函數能夠支持底層的socket是非阻塞模式的。但當將openssl和libuv進行結合時,會遇到一些問題:緩存

  1. openssl在進行數據讀寫以前,須要進行若干次「握手」。「握手」中會有若干次的數據讀寫。這個在普通的socket鏈接中是沒有的,在libuv的回調函數中須要進行處理。
  2. 因爲openssl須要對數據進行加密和解密,當openssl讀數據的時候,有可能會出現雖然加密的數據已經所有接收到本地了,但仍須要和遠端進行通訊來進一步確認如何解密數據。(會不會出現這個過程不太肯定。。。網上有文章說可能會出現,我也沒有仔細研究過openssl的實現細節,全部寧肯信其有,不可信其無吧。。。)

解決這兩個問題的思路是同樣的,將openssl看作是一個數據過濾器,可參考這篇文章安全

在和libuv結合時,openssl不能直接對socket進行讀寫,由於對socket的讀寫操做已經被libuv徹底封裝了。不過openssl能夠經過BIO進行讀寫數據。也就是說,須要準備兩個BIO,一個用於存儲openssl加密好的數據,一個用於存儲接收到的加密數據以備openssl解密。這個操做直接調用下面這個函數便可完成:socket

void SSL_set_bio(SSL *ssl, BIO *rbio, BIO *wbio);

設置好這兩個BIO以後,SSL_XXX系列函數的全部操做都是針對這兩個BIO,再也不直接和socket打交道。這樣對socket的操做就能夠委託給libuv了。tcp

對於寫數據到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的意圖。注意:這三個函數都有可能返回這兩個值。也就是說在讀數據的時候可能須要寫數據,在寫數據的時候可能須要讀數據。code

囉囉嗦嗦說了這麼多,上代碼纔是王道。如下代碼只是示意,並不能直接編譯運行^v^。
首先,聲明變量:blog

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^~

相關文章
相關標籤/搜索