用的方案是 Prism,cmark-gfm 本来就支持生成相关的 pre、code tag,并带上 "language-css" 这样的 attribute 值,那只要简单加上下面两行就好了,在项目站点上,选了几个自己觉得也许会用得比较多的语言,比如 Bash/Shell/JS/Lua/Go/Swift/Lisp,我也就大概会这么多而已吧。
<link rel="stylesheet" type="text/css" href="../styles/prism.min.css">
<script type="text/javascript" src="../js/prism.min.js"></script>
感觉效果很不错的,比如之前
选的主题是 tomorrow night,跟已有的主题搭配起来也挺不错,缺点可能是 js 代码 minify 后仍然有 60k 吧。
不管怎么说,还有 Etag 撑着呢,另外,毕竟是存放在 github 的,算是白嫖了。
先说一下背景,mnet 很早就想着加入 TLS 的支持,比如在做 cincau 或者 rpc_framework 的时候,就分别有作为独立 server 支持 https,以及作为 agent 使用内建的 TCP,去请求一个 https 认证的 API,或者拉取网页这样的要求。
但之前都是在 Lua/LuaJIT 这一层,在 C 之外做的 SSL 的状态管理,用起来不稳定,就放弃了,直接用了 curl。当时没有认真研究,自从考虑在 C 这一层,mnet 这一层加入 TLS 插件 后,之前不稳定的原因我终于找到了,这个后面再说。
先过一下 mnet 的插件系统,比如为什么要做成插件系统。
因为 mnet 本身是一个小型网络库,几乎是就是单文件支持 MacOS/Linux/FreeBSD/Windows,抽象了 epoll/kqueue/wepoll (IOCP epoll 化),将 TLS 做成插件,是希望没有编译链接 TLS、OpenSSL 时,仍能独立支持 TCP、UDP。而加入 TLS、OpenSSL 的插件代码,以及相关编译选项后,提供 TLS 的支持,使用方式跟抽象为 chann 的普通 TCP 一样。
为了达成这一个目标,mnet 将插件接口抽象为基于 chann_type 的一种配置,mnet_core 单文件本身就提供了 TCP、UDP 的内建插件,外部只需要在 open_chann 时指定使用 TCP、UDP 接口,就能使用 core 提供的 chann listen、connect、send、recv API。
TLS 插件则是抽象 TLS 为一种外部定义的 chann_type,配置了这个定义后,TLS chann 使用方式跟 TCP、UDP 没有差别,从内部往外看,TLS chann_type 也是一种普通的 chann 而已,内部 core 对于所有类型的 chann_type,都提供同样能力的配置接口,没有特别对待。
所谓插件配置,其实就是抽象了 open_chann,listen_chann/connect_chann, send_chann/recv_chann, disconnect_chann/close_chann,chann_state 的回调函数,mnet 的插件,只要能提供 fd,以及这些插件接口,就可以利用上内部的 epoll、kqueue,以及 sending cache。
这样,外部建立 TLS chann,只需要指定 TLS chann_type,open 后做 listen/connect 就好,插件需要外部提供 SSL_CTX,因此证书管理是在外部的,网络无关的 API 内部没有引入。插件提供了 filter 接口,对于 epoll/kqueue 的读写事件,会 filter 询问 chann_type 对应的插件接口,是否要传递给外层调用的 API,还是插件还需要继续处理。
插件同样封装了 chann_state,这样 TLS 的状态可以有 CLOSE/DISCONNECT/CONNECTING/CONNECTED/LISTEN 了。
接着说一下之前在 Lua/LuaJIT 层为何基于 mnet 的 TCP 层做 SSL 没成功,仔细阅读 SSL_read/SSL_write 的 man page,可以看到这个接口接收到 buffer 读写命令后,底层仍然可能有重协商的逻辑,重协商的逻辑需要双方的数据传递,因此有可能需要先读取数据(协商相关的数据)才能写 buffer,反观 SSL_read 底层的重协商有可能先要写数据,才能读。
SSL_read/SSL_write 都有可能导致底层重协商,如果 fd 是阻塞的,协商结束,数据读、写成功后,才会返回给调用方;如果是非阻塞的 fd,ret 返回 <= 0,需要调用 SSL_get_error(ret) 来获取是否是 SSL_ERROR_WANT_READ 或者 SSL_ERROR_WANT_WRITE,还是确实出错了,比如 SSL_ERROR_SSL 这种错误只有 SSL_shutdown,然后 close(fd) 了。
非阻塞 fd 导致的 WANT_READ 或 WANT_WRITE 的错误,需要调用方在合适的时机,使用同样的参数重新调用。说实话,看到这里,我总觉得这个 API 怎能这样设计,但是从 API 提供者的角度来说,非阻塞的 fd,需要打包额数据也许不足 ,也许干嘛干嘛,反正需要等待双方协商好后才能重新打包,如果传递过来的不是之前的数据,或者长度,又需要重新协商了。
为了方便调用方,SSL 提供了两个改善性的配置,一个是 SSL_MODE_ENABLE_PARTIAL_WRITE,只写一部分数据成功后,也返回了,再次发起 SSL_write 可以传递新的数据了,而不是整个 buffer 都成功才返回;另外一个是 SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER,意味着调用方可以换用 buffer 指针(内容、长度还需一致)。
之前 mnet 的 send 是有 cache 的,cache 长度是固定的,导致上面 SSL_write 这个接口不满足,长度变了,这次为了支持 TLS,一旦有 send 数据 cache,都是申请足量的内存 cache 住,后续 epoll/kqueue raise 了可以写的事件,在传递同样的内容、长度(仅 buffer 指针变化)给插件层的 send。
以上就解决了之前 Lua/LuaJIT SSL 不稳定的问题。
我自己是搞了自签名的证书、私钥测试了 reconnect 和 rwdata 这两个跟 TCP 一样的单点测试程序,算是通过了验收。因为 SSL_CTX 是外部提供的,因此后续的认证问题,应该不是问题,即便有特殊需要,基于插件的系统,应该可以可以回调解决吧,毕竟其实我也没有很重度使用这个插件的功能呢。
先这样吧。
之前我还提到使用了 torch 的 LuaRock 方案,但实际上不晓得 LuaRocks 在哪个版本起(至少在 3.7),编译的时候指定 Lua 解释器,就能直接支持,我其实已经用上很长时间了。
说实话,比较期待 LuaRocks 的方面是,直接根据依赖的 rocks,能够输出 single executable,虽然按照目前 LuaRocks 的 spec 构建描述,应该是很难的。