一、概述
在 Acl 的網絡通信模塊中,爲了支持安全網絡傳輸,引入了第三方 SSL 庫,當前支持 Polarssl 及其升級版 MbedTLS,Acl 庫中通過抽象與封裝,大大簡化了 SSL 的使用過程(現在開源的 SSL 庫使用確實太複雜了),以下是在 Acl 庫中使用 SSL 的特點:
- 爲了不給不使用 SSL 功能的用戶造成編譯上的障礙,Acl 庫採用動態加載 SSL 動態庫方式,這樣在連接時就不必提供 SSL 庫(當然,通過設置編譯開關,也允許用戶採用靜態連接 SSL 庫的方式);
- 在 Acl 的工程中,僅包含了指定版本的 Polarssl/Mbedtls 頭文件(在 acl/include/ 目錄下),這些頭文件在編譯 Acl 的 SSL 模塊時會使用到,且不對外暴露,因此使用者需要自行提供對應版本的 SSL 動態二進制庫(SSL庫的源代碼可以去官方 https://tls.mbed.org/ 下載,或者去 https://github.com/acl-dev/third_party 處下載);
- 在 Acl SSL 模塊中,分爲全局配置類和 IO 通信類,配置類對象只需在程序啓動時進行創建與初始化,且整個進程中按單例方式使用;IO 通信類對象與每一個 TCP 連接所對應的 socket 進行綁定,TCP 連接建立時進行初始化,進行 SSL 握手並接管 IO 過程;
- Acl SSL 模塊支持服務端及客戶端方式,在服務端模塊時需要加載數字證書及證書私鑰;
- Acl SSL 模塊支持阻塞與非阻塞兩種通信方式,阻塞方式還可以用在 Acl 協程通信中;
- Acl SSL 模塊已經應用在 Acl HTTP 通信中,從而方便用戶編寫支持 HTTPS/Websocket 的客戶端或服務端程序;同時,Acl SSL 模塊也給 Acl Redis 模塊提供了安全通信功能;
- Acl SSL 模塊是線程安全的,雖然官方提供的 Mbedtls 庫中增加支持線程安全的編譯選項,但其默認情況下卻是將此功能關閉的(這真是一個坑人的地方),當你需要打開線程支持功能時還必須得要提供線程鎖功能(通過函數回調註冊自己的線程鎖,好在 Acl 庫中有跨平臺的線程模塊),這應該是 Mbedtls 默認情況下不打開線程支持的原因;
- 當你使用 Mbedtls 時,建議從 https://github.com/acl-dev/third_party/tree/master/mbedtls-2.7.12 下載 Mbedtls 源碼編譯,此處的 Mbedtls 與官方的主要區別是:
- 在 config.h 中打開了線程安全的編譯選項,同時添加了用於線程安全的互斥鎖頭文件:threading_alt.h;
- Mbedtls 庫編譯後生成了三個庫文件:libmbedcrypto/libmbedx509/libmbedtls,而原來 Polarssl 只生成一個庫文件,所以爲了用戶使用方便,修改了 libray/CMakeLists.txt 文件,可以將這三個庫文件合併成一個;
- 增加了 visualc/VC2012(而官方僅提供了 VS2010),這樣在 Windows 平臺下可以使用 VS 2012 來編譯生成 mbedtls 庫。
二、API 接口說明
爲了支持更加通用的 SSL 接口,在 Acl SSL 模塊中定義了兩個基礎類:sslbase_conf
和 sslbase_io
,其中 ssbase_conf
類對象可以用做全局單一實例,ssbase_io
類對象用來給每一個 TCP socket 對象提供安全 IO 通信功能。
2.1、sslbase_conf 類
在 ssbase_conf 類中定義了純虛方法:open
,用來創建 SSL IO 通信類對象,在當前所支持 Polarssl 和 MbedTSL 中的配置類中(分別爲:acl::polarssl_conf
和 acl::mbedtls_conf
)均實現了該方法。下面是 open
方法的具體說明:
/**
* 純虛方法,創建 SSL IO 對象
* @param nblock {bool} 是否爲非阻塞模式
* @return {sslbase_io*}
*/
virtual sslbase_io* open(bool nblock) = 0;
在客戶端或服務端創建 SSL IO 對象(即:sslbase_io 對象)時調用,被用來與 TCP socket 進行綁定。下面是綁定過程:
bool bind_ssl_io(acl::socket_stream& conn, acl::sslbase_conf& ssl_conf)
{
// 創建一個阻塞式 SSL IO 對象
bool non_block = false;
acl::sslbase_io* ssl = ssl_conf.open(non_block);
// 將 SSL IO 對象與 TCP 連接流對象進行綁定,在綁定過程中會進行 SSL 握手,
// 如果 SSL 握手失敗,則返回該 SSL IO 對象,返回 NULL 表示綁定成功。
if (conn.setup_hook(ssl) == ssl) {
return false;
} else {
return true;
}
}
其中 acl::sslbase_io
的父類爲 acl::stream_hook
,在acl::stream
流基礎類中提供了方法setup_hook
用來註冊外部 IO 過程,其中的參數類型爲stream_hook
,通過綁定外部 IO 過程,將 SSL IO 過程與 acl 中的流處理 IO 過程進行綁定,從而使 acl 的 IO 流過程具備了 SSL 安全傳輸能力。
下面的幾個接口用在服務端進行證書及私鑰加載過程:
/**
* 添加一個服務端/客戶端自己的證書,可以多次調用本方法加載多個證書
* @param crt_file {const char*} 證書文件全路徑,非空
* @return {bool} 添加證書是否成功
*/
virtual bool add_cert(const char* crt_file);
/**
* 添加服務端/客戶端的密鑰(每個配置實例只需調用一次本方法)
* @param key_file {const char*} 密鑰文件全路徑,非空
* @param key_pass {const char*} 密鑰文件的密碼,沒有密鑰密碼可寫 NULL
* @return {bool} 設置是否成功
*/
virtual bool set_key(const char* key_file, const char* key_pass = NULL);
/**
* 當爲服務端模式時是否啓用會話緩存功能,有助於提高 SSL 握手效率
* @param on {bool} 是否在服務端啓用會話緩存方式
* 注:該函數僅對服務端模式有效
*/
virtual void enable_cache(bool on);
2.2、sslbase_io 類
acl::sslbase_io
類對象與每一個 TCP 連接對象 acl::socket_stream
進行綁定,使 acl::socket_stream
具備了進行 SSL 安全傳輸的能力,在 acl::sslbase_io
類中聲明瞭純虛方法handshake
,這使之成爲純虛類;另外,acl::sslbase_io
雖然繼承於acl::stream_hook
類,但並沒有實現 acl::stream_hook
中規定的四個純虛方法:open
,on_close
,read
,send
,這幾個虛方法也需要 acl::sslbase_io
的子類來實現,目前acl::sslbase_io
有兩個子類acl::polarssl_io
及acl::mbedtls_io
分別用來支持 Polarssl 及 MbedTLS。
下面是這幾個純虛方法的聲明:
/**
* ssl 握手純虛方法(屬於 sslbase_io 類)
* @return {bool} 返回 true 表示 SSL 握手成功,否則表示失敗
*/
virtual bool handshake(void) = 0;
下面幾個虛方法聲明於 acl::stream_hook
類中:
/**
* 讀數據接口
* @param buf {void*} 讀緩衝區地址,讀到的數據將存放在該緩衝區中
* @param len {size_t} buf 緩衝區大小
* @return {int} 讀到字節數,當返回值 < 0 時表示出錯
*/
virtual int read(void* buf, size_t len) = 0;
/**
* 發送數據接口
* @param buf {const void*} 發送緩衝區地址
* @param len {size_t} buf 緩衝區中數據的長度(必須 > 0)
* @return {int} 寫入的數據長度,返回值 <0 時表示出錯
*/
virtual int send(const void* buf, size_t len) = 0;
/**
* 在 stream/aio_stream 的 setup_hook 內部將會調用 stream_hook::open
* 過程,以便於子類對象用來初始化一些數據及會話
* @param s {ACL_VSTREAM*} 在 setup_hook 內部調用該方法將創建的流對象
* 作爲參數傳入
* @return {bool} 如果子類實例返回 false,則 setup_hook 調用失敗且會恢復原樣
*/
virtual bool open(ACL_VSTREAM* s) = 0;
/**
* 當 stream/aio_stream 流對象關閉前將會回調該函數以便於子類實例做一些善後工作
* @param alive {bool} 該連接是否依然正常
* @return {bool}
*/
virtual bool on_close(bool alive) { (void) alive; return true; }
/**
* 當 stream/aio_stream 對象需要釋放 stream_hook 子類對象時調用此方法
*/
virtual void destroy(void) {}
以上幾個虛方法均可以在 acl::polarssl_io
及 acl::mbedtls_io
中看到被實現。
三、編程示例
2.1、服務器模式(使用 MbedTLS)
首先給出一個完整的支持 SSL 的服務端例子,該例子使用了 MbedTLS 做爲 SSL 庫,如果想切換成 Polarssl 也非常簡單,方法類似(該示例位置:https://github.com/acl-dev/acl/tree/master/lib_acl_cpp/samples/ssl/server):
#include <assert.h>
#include "lib_acl.h"
#include "acl_cpp/lib_acl.hpp"
class echo_thread : public acl::thread {
public:
echo_thread(acl::sslbase_conf& ssl_conf, acl::socket_stream* conn)
: ssl_conf_(ssl_conf), conn_(conn) {}
private:
acl::sslbase_conf& ssl_conf_;
acl::socket_stream* conn_;
~echo_thread(void) { delete conn_; }
// @override
void* run(void) {
conn_->set_rw_timeout(60);
// 給 socket 安裝 SSL IO 過程
if (!setup_ssl()) {
return NULL;
}
do_echo();
delete this;
return NULL;
}
bool setup_ssl(void) {
bool non_block = false;
acl::sslbase_io* ssl = ssl_conf_.open(non_block);
// 對於使用 SSL 方式的流對象,需要將 SSL IO 流對象註冊至網絡
// 連接流對象中,即用 ssl io 替換 stream 中默認的底層 IO 過程
if (conn_->setup_hook(ssl) == ssl) {
printf("setup ssl IO hook error!\r\n");
ssl->destroy();
return false;
}
return true;
}
void do_echo(void) {
char buf[4096];
while (true) {
int ret = conn_->read(buf, sizeof(buf), false);
if (ret == -1) {
break;
}
if (conn_->write(buf, ret) == -1) {
break;
}
}
}
};
static void start_server(const acl::string addr, acl::sslbase_conf& ssl_conf) {
acl::server_socket ss;
if (!ss.open(addr)) {
printf("listen %s error %s\r\n", addr.c_str(), acl::last_serror());
return;
}
while (true) {
acl::socket_stream* conn = ss.accept();
if (conn == NULL) {
printf("accept error %s\r\n", acl::last_serror());
break;
}
acl::thread* thr = new echo_thread(ssl_conf, conn);
thr->set_detachable(true);
thr->start();
}
}
static bool ssl_init(const acl::string& ssl_crt, const acl::string& ssl_key,
acl::mbedtls_conf& ssl_conf) {
ssl_conf.enable_cache(true);
// 加載 SSL 證書
if (!ssl_conf.add_cert(ssl_crt)) {
printf("add ssl crt=%s error\r\n", ssl_crt.c_str());
return false;
}
// 設置 SSL 證書私鑰
if (!ssl_conf.set_key(ssl_key)) {
printf("set ssl key=%s error\r\n", ssl_key.c_str());
return false;
}
return true;
}
static void usage(const char* procname) {
printf("usage: %s -h [help]\r\n"
" -s listen_addr\r\n"
" -L ssl_libs_path\r\n"
" -c ssl_crt\r\n"
" -k ssl_key\r\n", procname);
}
int main(int argc, char* argv[]) {
acl::string addr = "0.0.0.0|2443";
#if defined(__APPLE__)
acl::string ssl_lib = "../libmbedtls_all.dylib";
#elif defined(__linux__)
acl::string ssl_lib = "../libmbedtls_all.so";
#elif defined(_WIN32) || defined(_WIN64)
acl::string ssl_path = "../mbedtls.dll";
acl::acl_cpp_init();
#else
# error "unknown OS type"
#endif
acl::string ssl_crt = "../ssl_crt.pem", ssl_key = "../ssl_key.pem";
int ch;
while ((ch = getopt(argc, argv, "hs:L:c:k:")) > 0) {
switch (ch) {
case 'h':
usage(argv[0]);
return 0;
case 's':
addr = optarg;
break;
case 'L':
ssl_lib = optarg;
break;
case 'c':
ssl_crt = optarg;
break;
case 'k':
ssl_key = optarg;
break;
default:
break;
}
}
acl::log::stdout_open(true);
// 設置 MbedTLS 動態庫路徑
const std::vector<acl::string>& libs = ssl_lib.split2(",; \t");
if (libs.size() == 1) {
acl::mbedtls_conf::set_libpath(libs[0]);
} else if (libs.size() == 3) {
// libcrypto, libx509, libssl);
acl::mbedtls_conf::set_libpath(libs[0], libs[1], libs[2]);
} else {
printf("invalid ssl_lib=%s\r\n", ssl_lib.c_str());
return 1;
}
// 加載 MbedTLS 動態庫
if (!acl::mbedtls_conf::load()) {
printf("load %s error\r\n", ssl_lib.c_str());
return 1;
}
// 初始化服務端模式下的全局 SSL 配置對象
bool server_side = true;
// SSL 證書校驗級別
acl::mbedtls_verify_t verify_mode = acl::MBEDTLS_VERIFY_NONE;
acl::mbedtls_conf ssl_conf(server_side, verify_mode);
if (!ssl_init(ssl_crt, ssl_key, ssl_conf)) {
printf("ssl_init failed\r\n");
return 1;
}
start_server(addr, ssl_conf);
return 0;
}
關於該示例有以下幾點說明:
- 該服務端例子使用了 MbedTLS 庫;
- 採用了動態加載 MbedTLS 動態庫的方式;
- 動態加載時需要設置 MbedTLS 動態庫的路徑,然後再加載,但在設置動態庫的路徑時卻有兩種方式,之所以有兩種設置 MbedTLS 動態庫路徑的方法,主要是因爲原來的 Polarssl 只生成一個庫,而到 MbedTLS 後卻生成了三個庫:libmbedcrypto,libmbedx509 和 libmbedtls,其中的依賴關係是 libmbedx509 依賴於 libmbedcrypto,libmbedtls 依賴於 libmbedx509 和 libmbedcrypto;但在 Windows 平臺上,官方卻只提供了生成一個庫(將這三個庫合併)的工程;因此,在 acl::mbedtls_conf 中在加載動態庫時,提供兩種方式,一個接口是用來設置三個庫的位置並加載,另一個接口用來設置一個統一庫的位置度加載;
- 該例子大體處理流程:
- 通過
acl::mbedtls_conf::set_libpath
方法設置 MbedTLS 的三個動態庫或一個統一的動態庫,然後調用acl::mbedtls_conf::load
加載動態庫; - 在
ssl_init
函數中,調用基類acl::sslbase_conf
中的虛方法add_cert
及set_key
分別用來加載 SSL 數字證書及證書私鑰; - 在
start_server
函數中,監聽本地服務地址,每接收一個 TCP 連接(對應一個acl::socket_stream
對象)便啓動一個線程進行 echo 過程; - 在客戶端處理線程中,調用
echo_thread::setup_ssl
方法給該acl::socket_stream
TCP 流對象綁定一個 SSL IO 對象,即:先通過調用acl::mbedtls_conf::open
方法創建一個acl::mbedtls_io
SSL IO 對象,然後通過acl::socket_stream
的基類中的方法set_hook
將該 SSL IO 對象與 TCP 流對象進行綁定並完成 SSL 握手過程; - SSL 握手成功後進入到
echo_thread::do_echo
函數中進行簡單的 SSL 安全 echo 過程。
- 通過
2.2、客戶端模式(使用 MbedTLS)
在熟悉了上面的 SSL 服務端編程後,下面給出使用 SSL 進行客戶端編程的示例(該示例位置:https://github.com/acl-dev/acl/tree/master/lib_acl_cpp/samples/ssl/client):
#include <assert.h>
#include "lib_acl.h"
#include "acl_cpp/lib_acl.hpp"
class echo_thread : public acl::thread {
public:
echo_thread(acl::sslbase_conf& ssl_conf, const char* addr, int count)
: ssl_conf_(ssl_conf), addr_(addr), count_(count) {}
~echo_thread(void) {}
private:
acl::sslbase_conf& ssl_conf_;
acl::string addr_;
int count_;
private:
// @override
void* run(void) {
acl::socket_stream conn;
conn.set_rw_timeout(60);
if (!conn.open(addr_, 10, 10)) {
printf("connect %s error %s\r\n",
addr_.c_str(), acl::last_serror());
return NULL;
}
// 給 socket 安裝 SSL IO 過程
if (!setup_ssl(conn)) {
return NULL;
}
do_echo(conn);
return NULL;
}
bool setup_ssl(acl::socket_stream& conn) {
bool non_block = false;
acl::sslbase_io* ssl = ssl_conf_.open(non_block);
// 對於使用 SSL 方式的流對象,需要將 SSL IO 流對象註冊至網絡
// 連接流對象中,即用 ssl io 替換 stream 中默認的底層 IO 過程
if (conn.setup_hook(ssl) == ssl) {
printf("setup ssl IO hook error!\r\n");
ssl->destroy();
return false;
}
printf("ssl setup ok!\r\n");
return true;
}
void do_echo(acl::socket_stream& conn) {
const char* data = "hello world!\r\n";
int i;
for (i = 0; i < count_; i++) {
if (conn.write(data, strlen(data)) == -1) {
break;
}
char buf[4096];
int ret = conn.read(buf, sizeof(buf) - 1, false);
if (ret == -1) {
printf("read over, count=%d\r\n", i + 1);
break;
}
buf[ret] = 0;
if (i == 0) {
printf("read: %s", buf);
}
}
printf("thread-%lu: count=%d\n", acl::thread::self(), i);
}
};
static void start_clients(acl::sslbase_conf& ssl_conf, const acl::string addr,
int cocurrent, int count) {
std::vector<acl::thread*> threads;
for (int i = 0; i < cocurrent; i++) {
acl::thread* thr = new echo_thread(ssl_conf, addr, count);
threads.push_back(thr);
thr->start();
}
for (std::vector<acl::thread*>::iterator it = threads.begin();
it != threads.end(); ++it) {
(*it)->wait(NULL);
delete *it;
}
}
static void usage(const char* procname) {
printf("usage: %s -h [help]\r\n"
" -s listen_addr\r\n"
" -L ssl_libs_path\r\n"
" -c cocurrent\r\n"
" -n count\r\n", procname);
}
int main(int argc, char* argv[]) {
acl::string addr = "0.0.0.0|2443";
#if defined(__APPLE__)
acl::string ssl_lib = "../libmbedtls_all.dylib";
#elif defined(__linux__)
acl::string ssl_lib = "../libmbedtls_all.so";
#elif defined(_WIN32) || defined(_WIN64)
acl::string ssl_path = "../mbedtls.dll";
acl::acl_cpp_init();
#else
# error "unknown OS type"
#endif
int ch, cocurrent = 10, count = 10;
while ((ch = getopt(argc, argv, "hs:L:c:n:")) > 0) {
switch (ch) {
case 'h':
usage(argv[0]);
return 0;
case 's':
addr = optarg;
break;
case 'L':
ssl_lib = optarg;
break;
case 'c':
cocurrent = atoi(optarg);
break;
case 'n':
count = atoi(optarg);
break;
default:
break;
}
}
acl::log::stdout_open(true);
// 設置 MbedTLS 動態庫路徑
const std::vector<acl::string>& libs = ssl_lib.split2(",; \t");
if (libs.size() == 1) {
acl::mbedtls_conf::set_libpath(libs[0]);
} else if (libs.size() == 3) {
// libcrypto, libx509, libssl);
acl::mbedtls_conf::set_libpath(libs[0], libs[1], libs[2]);
} else {
printf("invalid ssl_lib=%s\r\n", ssl_lib.c_str());
return 1;
}
// 加載 MbedTLS 動態庫
if (!acl::mbedtls_conf::load()) {
printf("load %s error\r\n", ssl_lib.c_str());
return 1;
}
// 初始化客戶端模式下的全局 SSL 配置對象
bool server_side = false;
// SSL 證書校驗級別
acl::mbedtls_verify_t verify_mode = acl::MBEDTLS_VERIFY_NONE;
acl::mbedtls_conf ssl_conf(server_side, verify_mode);
start_clients(ssl_conf, addr, cocurrent, count);
return 0;
}
在客戶方式下使用 SSL 時的方法與服務端時相似,不同之處是在客戶端下使用 SSL 時不必加載證書和設置私鑰。
2.3、非阻塞模式
在使用 SSL 進行非阻塞編程時,動態庫的加載、證書的加載及設置私鑰過程與阻塞式 SSL 編程方法相同,不同之處在於創建 SSL IO 對象時需要設置爲非阻塞方式,另外在 SSL 握手階段需要不斷檢測 SSL 握手是否成功,下面只給出相關不同之處,完整示例可以參考:https://github.com/acl-dev/acl/tree/master/lib_acl_cpp/samples/ssl/aio_server,https://github.com/acl-dev/acl/tree/master/lib_acl_cpp/samples/ssl/aio_client):
- 調用
acl::sslbase_conf
中的虛方法open
時傳入的參數爲true
表明所創建的 SSL IO 對象爲非阻塞方式; - 在創建非阻塞 IO 對象後,需要調用
acl::aio_socket_stream
中的read_wait
方法,以便可以觸發acl::aio_istream::read_wakeup
回調,從而在該回調裏完成 SSL 握手過程; - 在非阻塞IO的讀回調裏需要調用
acl::sslbase_io
中的虛方法handshake
嘗試進行 SSL 握手並通過handshake_ok
檢測握手是否成功。
下面給出在read_wakeup
回調裏進行 SSL 握手的過程:
bool read_wakeup()
{
acl::sslbase_io* hook = (acl::sslbase_io*) client_->get_hook();
if (hook == NULL) {
// 非 SSL 模式,異步讀取數據
//client_->read(__timeout);
client_->gets(__timeout, false);
return true;
}
// 嘗試進行 SSL 握手
if (!hook->handshake()) {
printf("ssl handshake failed\r\n");
return false;
}
// 如果 SSL 握手已經成功,則開始按行讀數據
if (hook->handshake_ok()) {
// 由 reactor 模式轉爲 proactor 模式,從而取消
// read_wakeup 回調過程
client_->disable_read();
// 異步讀取數據,將會回調 read_callback
//client_->read(__timeout);
client_->gets(__timeout, false);
return true;
}
// SSL 握手還未完成,等待本函數再次被觸發
return true;
}
在該代碼片斷中,如果 SSL 握手一直處於進行中,則 read_wakeup
可能會被調用多次,這就意味着 handshake
握手過程也會被調用多次,然後再通過 handshake_ok
判斷握手是否已經成功,如果成果,則通過調用 gets
方法切換到 IO 過程(該 IO 過程對應的回調爲 read_callback
),否則進行 SSL 握手過程(繼續等待 read_wakeup
被回調)。