在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解密。這個操作直接調用下面這個函數即可完成:

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~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM