Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions src/net/uvcpp_tcp_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <req/uvcpp_getaddrinfo.h>
#include <uvcpp/uvcpp_alloc.h>
#include <uvcpp/uvcpp_define.h>
#include <handle/uvcpp_timer.h> // 握手超时(见 tls_arm_handshake_timer)

#include <chrono>
#include <cstdio>
Expand Down Expand Up @@ -153,6 +154,17 @@ uvcpp_tcp_client::~uvcpp_tcp_client() {
// TLS 读路径上 `tls_ssl_` 正压在栈上(`tls_feed` / `tls_flush_out` 还在
// 下面那几层里),不能删。
tls_ssl_ = nullptr;

// 握手超时定时器:先停掉(停是安全的,从它自己的回调里停也行)。
// 释放要分情况 —— 如果本对象正是在那个超时回调里被拆掉的,删除句柄等于
// 析构正在执行的闭包;那种情况只停不放,让句柄随循环一起走。
if (tls_hs_timer_ != nullptr) {
tls_hs_timer_->stop();
if (!tls_hs_in_cb_) {
delete tls_hs_timer_;
}
tls_hs_timer_ = nullptr;
}
#endif
loop_ = nullptr;
tcp_ = nullptr;
Expand All @@ -170,6 +182,10 @@ uvcpp_tcp_client::~uvcpp_tcp_client() {
}

#if UVCPP_OPENSSL_ENABLE
// 握手超时定时器(惰性创建,握手成功即释放;只有超时那一条路上会活到
// 这里)。这条分支不在任何回调栈上,可以安全释放。
tls_disarm_handshake_timer();

// TLS 对象归本客户端所有,跟着一起走。注意这里**不发 close_notify**:
// 发它要先把 wbio 里的字节异步写出去,而析构路径上没有事件循环可等。
// 所以对端看到的是 TCP 层的断开(`SSL_ERROR_SYSCALL` / 提前 EOF)而不是
Expand Down Expand Up @@ -1822,6 +1838,52 @@ void uvcpp_tcp_client::set_tls_ready_callback(

int uvcpp_tcp_client::tls_last_ssl_error() const { return tls_ssl_error_; }

void uvcpp_tcp_client::set_tls_handshake_timeout_ms(int ms) {
tls_hs_timeout_ms_ = ms > 0 ? ms : 0;
}

void uvcpp_tcp_client::tls_arm_handshake_timer() {
// 只有"握手已经开始、且还没结束"才需要计时。tls_drive_handshake() 每次读
// 事件都会调进来,所以这里必须廉价且幂等。
if (tls_hs_timeout_ms_ <= 0) return;
if (tls_handshake_done_ || tls_ssl_ == nullptr) return;
if (tls_hs_timer_ != nullptr) return; // 已经在计了,不重起
if (loop_ == nullptr) return;

tls_hs_timer_ = new uvcpp_timer(loop_);
tls_hs_timer_->start(
[this](uvcpp_timer*) {
// 记下"此刻正跑在超时回调里"。析构要用它判断能不能安全释放这个句柄:
// 本回调下面的 tls_fail() 会把整条连接拆掉、把本对象释放掉,而那一切
// 都发生在**这个回调的栈帧还在**的时候 —— 那时析构它就是在踩正在执行
// 的闭包。
tls_hs_in_cb_ = true;

// **回调里只 stop,不 delete**,理由同上。
if (tls_hs_timer_ != nullptr) tls_hs_timer_->stop();
if (tls_handshake_done_) return;

std::fprintf(stderr,
"[uvcpp_tcp_client] TLS handshake timed out after %d ms, "
"closing connection\n",
tls_hs_timeout_ms_);
// 走与"握手失败"完全相同的那条路:通知上层(绝不把连接交付出去)
// → 读侧收尾 → 关闭回调 → 服务端摘表并释放本对象。
//
// **必须是本回调的最后一句**:tls_fail 会一路走到释放本对象,
// 之后再碰任何成员都是 use-after-free。
tls_fail(UV_ETIMEDOUT, false);
},
static_cast<uint64_t>(tls_hs_timeout_ms_), 0); // repeat=0:一次性
}

void uvcpp_tcp_client::tls_disarm_handshake_timer() {
if (tls_hs_timer_ == nullptr) return;
tls_hs_timer_->stop();
delete tls_hs_timer_;
tls_hs_timer_ = nullptr;
}

bool uvcpp_tcp_client::set_tls_alpn_protos(const std::vector<std::string>& protos) {
if (tls_handshake_done_) return false; // 协商已经过去了,改不了
tls_alpn_want_ = protos;
Expand All @@ -1832,6 +1894,10 @@ bool uvcpp_tcp_client::set_tls_alpn_protos(const std::vector<std::string>& proto
int uvcpp_tcp_client::tls_drive_handshake() {
if (tls_ssl_ == nullptr || tls_handshake_done_) return 0;

// 握手从这一刻起算超时(幂等)。放在这里而不是 enable_tls() 里,是因为
// 服务端与客户端两条路都汇聚到本函数推进握手 —— 一处起算,两处生效。
tls_arm_handshake_timer();

const int rc = tls_ssl_->handshake();
if (rc < 0) {
// 真出错(不是 WANT_READ/WANT_WRITE —— 那两个在本层被归一化成 0)。
Expand All @@ -1842,6 +1908,8 @@ int uvcpp_tcp_client::tls_drive_handshake() {
}
if (rc == 1) {
tls_handshake_done_ = true;
// 握手结束了,这条连接不再需要那道闸门。正常路径上定时器只活了几十微秒。
tls_disarm_handshake_timer();
tls_ssl_error_ = tls_ssl_->last_ssl_error();
// 在**这里**取 ALPN 结果,而不是等上层来问:ready 回调的契约是"回调里
// 对象可能已被析构",从回调里反查 SSL* 不安全。这里是唯一置真点、被五个
Expand Down Expand Up @@ -2090,6 +2158,15 @@ void uvcpp_tcp_client::tls_finish_write(int status) {
}

void uvcpp_tcp_client::tls_fail(int err, bool peer_closed) {
// 任何一条失败路径上都不再需要握手超时。**放在最前面**:下面每一条分支都
// 可能把本对象释放掉,之后再碰成员就是 use-after-free。
//
// 这里**只 stop、不 delete**:本函数可能是从定时器回调里被调用的(超时那
// 一条路),而在回调压栈期间析构这个句柄就是踩正在执行的闭包 —— 与析构里
// 那段"三层悬垂"记的是同一件事。释放留给析构(正常路径)或"析构时发现
// 自己在回调里"那一支(只停不放,见 ~uvcpp_tcp_client)。
if (tls_hs_timer_ != nullptr) tls_hs_timer_->stop();

const int code = (err != 0) ? err : (peer_closed ? UV_EOF : UV_EPROTO);
if (err != 0) {
last_error_code_ = err;
Expand Down
58 changes: 58 additions & 0 deletions src/net/uvcpp_tcp_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ namespace uvcpp {
class uvcpp_ssl; // 见 src/ssl/uvcpp_ssl.h
class uvcpp_ssl_context;
#endif
class uvcpp_timer; // 见 src/handle/uvcpp_timer.h(握手超时用,只存指针)

/**
* @brief Bitmask status flags for TCP client lifecycle.
Expand Down Expand Up @@ -223,6 +224,33 @@ class UVCPP_API uvcpp_tcp_client {
* 触发,所以回调里看到的是一个还活着的对象。
*/
void set_tls_ready_callback(std::function<void(uvcpp_tcp_client*, int)> cb);

/**
* @brief TLS **握手阶段**的超时(毫秒)。**0 = 不设超时**。默认 10000。
*
* 为什么必须有它
* --------------
* 服务端 accept 之后要等握手成功才把连接交给上层(见 `set_tls_ready_callback`
* 的说明),而**在这之前这条连接不在上层任何登记表里** —— webapp 那一层的
* 闲置扫描遍历的是自己的连接表,握手期的连接根本不在里面,`idle_timeout_ms`
* 因此管不到它。
*
* 于是有一条明文的 slowloris 覆盖不到的路:客户端连上之后,每 500 ms 发一个
* 字节去喂 ClientHello,握手永远差一点不完成。这条连接会连同它的 SSL 对象、
* 读写缓冲区**无限期挂着**,而占用的带宽近乎为零。明文那条路有
* `idle_timeout_ms` 兜着(判据是"从请求的第一个字节起算"),但那条规则的前提
* 是"连接已经在登记表里" —— 握手期的连接不满足这个前提。
*
* 所以这不是 `idle_timeout_ms` 的替代品,而是它**覆盖不到的那一段**的补位:
* 上限从握手开始(第一次 `tls_drive_handshake()`)起算,握手一结束(无论成功
* 还是失败)就撤销。
*
* @note 客户端一侧同样生效 —— "连上了但对端永远不把握手走完"也是同一种占用。
*/
void set_tls_handshake_timeout_ms(int ms);

/** @brief 当前的握手超时;0 表示不设。 */
int tls_handshake_timeout_ms() const { return tls_hs_timeout_ms_; }
#endif // UVCPP_OPENSSL_ENABLE

// -----------------------------------------------------------------
Expand Down Expand Up @@ -749,6 +777,36 @@ class UVCPP_API uvcpp_tcp_client {
bool tls_write_sync_ = false; ///< 上面那个写走的是同步接口
int tls_write_status_ = 0; ///< 上面那个写的最终状态
std::function<void(uvcpp_tcp_client*, int)> tls_ready_cb_; ///< 握手结束通知一次

/** 起算握手超时。幂等:已经起过就不重起(每次读事件都会调一次)。 */
void tls_arm_handshake_timer();
/**
* @brief 停掉握手超时**并释放句柄**。
*
* @warning **绝不能在定时器自己的回调里调它** —— 那等于在回调压栈期间把
* 正在执行的那个闭包析构掉(与本文件析构注释里记的"三层悬垂"是
* 同一类)。要停就从回调里直接 `tls_hs_timer_->stop()`,或者走
* `tls_fail()`(它只 stop、不释放)。
*/
void tls_disarm_handshake_timer();

/**
* @brief 握手超时用的定时器。
*
* 惰性创建(第一次推进握手时),握手成功即释放 —— 正常路径上它只活几十
* 微秒。只有"真的超时了"这一条路上它会活到连接被拆掉为止,那时析构会收掉。
*/
uvcpp_timer* tls_hs_timer_ = nullptr;
/** 握手超时毫秒数;0 = 不设。 */
int tls_hs_timeout_ms_ = 10000;
/**
* @brief 此刻是否正跑在握手超时的回调里。
*
* 只给析构看:超时回调会一路走到 `tls_fail()` → 拆连接 → 释放本对象,而
* 那一切都在**这个回调的栈帧还在**的时候发生。析构据此决定 `tls_hs_timer_`
* 是能安全 delete,还是只能 stop 之后留着(否则就是析构一个正在执行的回调)。
*/
bool tls_hs_in_cb_ = false;
#endif // UVCPP_OPENSSL_ENABLE
};

Expand Down
9 changes: 9 additions & 0 deletions src/net/uvcpp_tcp_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,17 @@ int uvcpp_tcp_server::listen(
// `delete`:此刻句柄是活的,且已经进了登记表。
bool tls_handshake_pending = false;
if (ssl_ctx_ != nullptr) {
// 握手超时**必须在 enable_tls 之前设**:`enable_tls()` 里就会推进
// 一次握手,而超时是从"第一次推进"起算的 —— 设晚了对这一步不生效。
client->set_tls_handshake_timeout_ms(tls_hs_timeout_ms_);

const int tls_rc = client->enable_tls(ssl_ctx_);
if (tls_rc != 0) {
last_error_code_ = tls_rc;
close_client_on_callback_error(client);
return;
}

tls_handshake_pending = !client->is_tls_handshake_done();
if (tls_handshake_pending) {
// 通知只是把 `on_connection` 挂到"握手成功"那一刻。
Expand Down Expand Up @@ -425,6 +430,10 @@ void uvcpp_tcp_server::set_ssl_context(uvcpp_ssl_context* ctx) {
ssl_ctx_ = ctx;
}

void uvcpp_tcp_server::set_tls_handshake_timeout_ms(int ms) {
tls_hs_timeout_ms_ = ms > 0 ? ms : 0;
}

void uvcpp_tcp_server::on_tls_handshake_done(uvcpp_tcp_client* client,
int status) {
if (client == nullptr) return;
Expand Down
18 changes: 18 additions & 0 deletions src/net/uvcpp_tcp_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,22 @@ class UVCPP_API uvcpp_tcp_server {

/** @brief 当前的 TLS 上下文;没装 TLS 时为 nullptr。 */
uvcpp_ssl_context* ssl_context() const { return ssl_ctx_; }

/**
* @brief TLS 握手的超时(毫秒)。0 = 不设。默认 10000。装 TLS 才有意义。
*
* 服务端在握手成功之前**不会**把连接交给上层(见 `uvcpp_tcp_client::
* set_tls_ready_callback`),所以这期间这条连接不在上层的连接表里,上层的
* 闲置超时看不到它。没有这道闸门时,一个「连上之后每 500 ms 发一个字节喂
* ClientHello、永远不把握手做完」的客户端可以让连接无限期挂着。
*
* 值会传给之后 accept 的每一条连接;已经建立的连接不受影响。
* @warning 必须在 loop 线程调用。
*/
void set_tls_handshake_timeout_ms(int ms);

/** @brief 当前的 TLS 握手超时;0 表示不设。 */
int tls_handshake_timeout_ms() const { return tls_hs_timeout_ms_; }
#endif // UVCPP_OPENSSL_ENABLE

// -----------------------------------------------------------------
Expand Down Expand Up @@ -423,6 +439,8 @@ class UVCPP_API uvcpp_tcp_server {

/** @brief 服务端级 TLS 上下文(非拥有)。nullptr = 明文。 */
uvcpp_ssl_context* ssl_ctx_ = nullptr;
/** TLS 握手超时(毫秒);0 = 不设。accept 时传给每条新连接。 */
int tls_hs_timeout_ms_ = 10000;
#endif // UVCPP_OPENSSL_ENABLE

// Trampoline — C-style fn ptr + void* to avoid MSVC std::function
Expand Down
10 changes: 10 additions & 0 deletions src/webapp/uvcpp_web_app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,16 @@ bool uvcpp_web_app::ssl_enabled() const {
return ssl_requested_ && static_cast<bool>(ssl_ctx_);
}

uvcpp_web_app& uvcpp_web_app::set_tls_handshake_timeout_ms(int ms) {
// `http_` 在构造时就建好了,所以这里拿得到 tcp_server —— 与
// `set_max_pipelined_requests` 那类"只改配置、等 start() 生效"的 setter
// 不同,这一条是**立刻写进 tcp_server** 的:tcp_server 在 accept 时才读它,
// 只要在 start() 之前调就来得及。
uvcpp_tcp_server* ts = tcp_server();
if (ts != nullptr) ts->set_tls_handshake_timeout_ms(ms);
return *this;
}

#endif // UVCPP_OPENSSL_ENABLE

// =========================================================================
Expand Down
20 changes: 20 additions & 0 deletions src/webapp/uvcpp_web_app.h
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,26 @@ class UVCPP_API uvcpp_web_app : public uvcpp_web_context_host {

/** @brief TLS 配置失败的原因(空 = 没出错)。 */
const std::string& ssl_error() const { return ssl_error_; }

/**
* @brief TLS 握手的超时(毫秒)。0 = 不设。默认 10000。
*
* **为什么 webapp 这一层需要它**:框架在握手成功之前不会把连接交给应用
* (所以 `on_connection` 与连接登记表都晚于握手),而闲置扫描遍历的正是
* 那张登记表 —— 于是**握手期的连接对 `idle_timeout_ms` 完全不可见**。
* 一个「连上之后每 500 ms 发一个字节喂 ClientHello、永远不把握手做完」的
* 客户端,可以让连接连同它的 SSL 对象与读写缓冲区无限期挂着,带宽占用近乎
* 为零。这一条补的就是 `idle_timeout_ms` 覆盖不到的那一段。
*
* 必须在 `start()` 之前调用(值会传给之后 accept 的每条连接)。
*/
uvcpp_web_app& set_tls_handshake_timeout_ms(int ms);

/** @brief 当前的 TLS 握手超时;0 表示不设。 */
int tls_handshake_timeout_ms() {
uvcpp_tcp_server* ts = tcp_server();
return ts != nullptr ? ts->tls_handshake_timeout_ms() : 0;
}
#endif // UVCPP_OPENSSL_ENABLE

// -----------------------------------------------------------------
Expand Down
Loading
Loading