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

在libuv中使用openssl建立ssl连接的更多相关文章

  1. 关于JDBC技术中,调用MySQL中不建议在没有服务器身份验证的情况下建立SSL连接错误解决

    今天学习到了JBDC前沿:对JDBC编写步骤的封装,出现了一大串红色报错(当然,也不能叫报错,毕竟不是所有的红色都是错误eeror,) 错误如下: Establishing SSL connectio ...

  2. 无法建立SSL连接

    在使用wget工具的过程中,当URL使用HTTPS协议时,经常出现如下错误:“无法建立SSL连接”. 这是因为wget在使用HTTPS协议时,默认会去验证网站的证书,而这个证书验证经常会失败.加上&q ...

  3. 苹果电脑利用wget总是会出现无法建立 SSL 连接的问题

    在做迁徙学习的过程中,需要下载已经训练好的Inception_v3模型,首先我们为了将下载的模型保存到指定的地方,我们需要利用 wget -P 想要保存的目录 模型的网址,例如 wget -P /Vo ...

  4. wget无法建立SSL连接

    在使用wget工具的过程中,当URL使用HTTPS协议时,经常出现如下错误:“无法建立SSL连接”. 这是因为wget在使用HTTPS协议时,默认会去验证网站的证书,而这个证书验证经常会失败.加上&q ...

  5. wget 无法建立ssl连接 [ERROR: certificate common name ?..ssl.fastly.net?.doesn?. match requested host name ?.ache.ruby-lang.org?. To connect to cache.ruby-lang.org insecurely, use ?.-no-check-certificate?]

    通过wget下载文件,报错 [root@Redmine-186 opt]# wget https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.6.tar.g ...

  6. 使用wget提示无法建立SSL连接

    wget 下载URL 提示无法建立SSL连接 解决方法: 原命令上加上" --no-check-certificate" 这是因为wget在使用HTTPS协议时,默认会去验证网站的 ...

  7. linux执行wget url时提示“无法建立 SSL 连接”

    linux执行wget url时提示“无法建立 SSL 连接” 原因: wget在使用HTTPS协议时,默认会去验证网站的证书,而这个证书验证经常会失败 解决方案: 1.加上参数“--no-check ...

  8. centos无法建立ssl连接

    在centos下使用wget安装mysql5.7时,提示无法建立ssl连接 查阅资料,在命令wget后加上 --no-check-certificate也还是无法建立SSL连接. 后来,觉得可能是由于 ...

  9. mysql中配置ssl_key、ssl-cert、ssl-ca的路径及建立ssl连接

    1.创建 CA 私钥和 CA 证书 (1)下载并安装openssl,将bin目录配置到环境变量: (2)设置openssl.cfg路径(若不设置会报错,找不到openssl配置文件) \bin\ope ...

随机推荐

  1. 用sysbench压测MySQL,通过orzdba监控MySQL

    1.1 安装sysbench wget https://codeload.github.com/akopytov/sysbench/zip/0.5 unzip 0.5 cd sysbench-0.5/ ...

  2. 解决Axis2在webservice中遇到特殊字符的无法传输的缺陷(<CDATA>数据类型)

    在使用Axis2进行soa webservice开发时,遇到类似以下的错误信息: com.ctc.wstx.sw.BaseStreamWriter.writeCharacters(BaseStream ...

  3. 170318 11:44:26 [ERROR] Can't start server: can't create PID file: No space left on device

    数据库挂了.打开远程,进了系统,service mysqld stop 失败.service mysqld start等了好大一会,提示Timeout error occurred trying to ...

  4. 453. Minimum Moves to Equal Array Elements 一次改2个数,变成统一的

    [抄题]: Given a non-empty integer array of size n, find the minimum number of moves required to make a ...

  5. 932F Escape Through Leaf

    传送门 题目大意 https://www.luogu.org/problemnew/show/CF932F 分析 我们可以从叶子向根每次插入b和ans 所以我们不难发现就是相当于插入线段 于是李超树+ ...

  6. Luogu 2403 [SDOI2010]所驼门王的宝藏

    BZOJ 1924 内存要算准,我MLE了两次. 建立$n + r + c$个点,对于一个点$i$的坐标为$(x, y)$,连边$(n + x, i)$和$(n + r + y, i)$,代表这一列和 ...

  7. 第八课 ROS的空间描述和变换

    1.tf的实际应用 1)在机器人的配置中 从上面可以看出激光雷达中心距离机器人底座的中心有20cm,激光雷达的中心距机器人底座中心有10cm,如果激光雷达在障碍物前面0.3米,那么机器人底座离障碍物多 ...

  8. WEB测试和APP测试区别

    Web测试和App测试从流程上来说,没有区别.都需要经历测试计划方案,用例设计,测试执行,缺陷管理,测试报告等相关活动.从技术上来说,WEB测试和APP测试其测试类型也基本相似,都需要进行功能测试.性 ...

  9. css总结6:行高和字体大小

    1 CSS line-height 属性 代码: p.small {line-height:70%}p.big {line-height:200%} 运行后:70%与200%宽高 2 CSS font ...

  10. Linq学习<三> linq to entity

    之前一直用sql选择出数据放在一个集合中,然后再用Linq或者lambda去操作数据,今天学了Linq to entity 才知道原来linq产生是为了Entity.也就是EDM(实体数据模型) 关于 ...